diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## v0.3.2
+
+API changes:
+* Added `hookModifyFileSpecs` to `Hooks` ([#59](https://github.com/brandonchinn178/skeletest/issues/59))
+
 ## v0.3.1
 
 API changes:
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -432,6 +432,7 @@
 
 Skeletest lets you hook into specific parts of test execution. Skeletest currently supports the following hooks:
 
+* `hookModifyFileSpecs` - Modify the specs in each file. Runs once per file, taking in the `[SpecTree]` containing all the specs in the file. This can be used to do your own test selection, test transformations, etc.
 * `hookRunTest` - Modify how/if a test is run. Takes the `TestInfo` of the currently running test. `TestInfo` contains `testInfoMarkers`, which you can query with `findMarker` or `hasMarkerNamed`.
 
 ### Plugins
diff --git a/skeletest.cabal b/skeletest.cabal
--- a/skeletest.cabal
+++ b/skeletest.cabal
@@ -1,7 +1,7 @@
 cabal-version: 3.0
 
 name: skeletest
-version: 0.3.1
+version: 0.3.2
 synopsis: Batteries-included, opinionated test framework
 description: Batteries-included, opinionated test framework. See README.md for more details.
 homepage: https://github.com/brandonchinn178/skeletest#readme
@@ -49,6 +49,7 @@
     Skeletest.Internal.Snapshot
     Skeletest.Internal.Spec
     Skeletest.Internal.Spec.Output
+    Skeletest.Internal.Spec.Tree
     Skeletest.Internal.TestInfo
     Skeletest.Internal.TestRunner
     Skeletest.Internal.TestTargets
diff --git a/src/Skeletest/Internal/Spec.hs b/src/Skeletest/Internal/Spec.hs
--- a/src/Skeletest/Internal/Spec.hs
+++ b/src/Skeletest/Internal/Spec.hs
@@ -57,6 +57,7 @@
   reportTestResultWithInlineMessage,
   reportTestResultWithoutMessage,
  )
+import Skeletest.Internal.Spec.Tree (SpecTree (..))
 import Skeletest.Internal.TestInfo (TestInfo (TestInfo), withTestInfo)
 import Skeletest.Internal.TestInfo qualified as TestInfo
 import Skeletest.Internal.TestRunner (
@@ -88,24 +89,6 @@
 withSpecTrees :: (Monad m) => ([SpecTree] -> m [SpecTree]) -> Spec -> m Spec
 withSpecTrees f = fmap (Spec . tell) . f . getSpecTrees
 
-data SpecTree
-  = SpecGroup
-      { groupLabel :: Text
-      , groupTrees :: [SpecTree]
-      }
-  | SpecTest
-      { testName :: Text
-      , testMarkers :: [SomeMarker]
-      -- ^ Markers, in order from least to most recently applied.
-      --
-      -- >>> withMarker MarkerA . withMarker MarkerB $ test ...
-      --
-      -- will contain
-      --
-      -- >>> SpecTest { testMarkers = [MarkerA, MarkerB] }
-      , testAction :: IO TestResult
-      }
-
 -- | Traverse the tree with the given processing function.
 --
 -- To preprocess trees with @pre@ and postprocess with @post@:
@@ -161,7 +144,8 @@
                 , testFile = specPath
                 }
         Text.putStrLn $ Text.pack specPath
-        runTrees emptyTestInfo $ getSpecTrees specSpec
+        specTrees <- hookModifyFileSpecs $ getSpecTrees specSpec
+        runTrees emptyTestInfo specTrees
  where
   Hooks{..} = builtinHooks <> hooks0
   builtinHooks = xfailHook <> skipHook
diff --git a/src/Skeletest/Internal/Spec/Tree.hs b/src/Skeletest/Internal/Spec/Tree.hs
new file mode 100644
--- /dev/null
+++ b/src/Skeletest/Internal/Spec/Tree.hs
@@ -0,0 +1,25 @@
+module Skeletest.Internal.Spec.Tree (
+  SpecTree (..),
+) where
+
+import Data.Text (Text)
+import Skeletest.Internal.Markers (SomeMarker)
+import Skeletest.Internal.TestRunner (TestResult)
+
+data SpecTree
+  = SpecGroup
+      { groupLabel :: Text
+      , groupTrees :: [SpecTree]
+      }
+  | SpecTest
+      { testName :: Text
+      , testMarkers :: [SomeMarker]
+      -- ^ Markers, in order from least to most recently applied.
+      --
+      -- >>> withMarker MarkerA . withMarker MarkerB $ test ...
+      --
+      -- will contain
+      --
+      -- >>> SpecTest { testMarkers = [MarkerA, MarkerB] }
+      , testAction :: IO TestResult
+      }
diff --git a/src/Skeletest/Plugin.hs b/src/Skeletest/Plugin.hs
--- a/src/Skeletest/Plugin.hs
+++ b/src/Skeletest/Plugin.hs
@@ -1,4 +1,5 @@
 module Skeletest.Plugin (
+  -- * Plugin
   Plugin (..),
   defaultPlugin,
 
@@ -22,13 +23,19 @@
   hasMarkerNamed,
 ) where
 
+import Control.Monad ((>=>))
 import Skeletest.Internal.CLI (Flag)
 import Skeletest.Internal.Markers (findMarker, hasMarkerNamed)
 import Skeletest.Internal.Snapshot (SnapshotRenderer)
 import Skeletest.Internal.Spec.Output (BoxSpec, BoxSpecContent (..))
+import Skeletest.Internal.Spec.Tree (SpecTree)
 import Skeletest.Internal.TestInfo (TestInfo (..))
 import Skeletest.Internal.TestRunner (TestResult (..), TestResultMessage (..))
 
+-- | A plugin for extending Skeletest.
+--
+-- Use 'defaultPlugin' instead of using v'Plugin' directly, to minimize
+-- breaking changes.
 data Plugin = Plugin
   { cliFlags :: [Flag]
   , snapshotRenderers :: [SnapshotRenderer]
@@ -54,14 +61,22 @@
     , hooks = defaultHooks
     }
 
+-- | Hooks for extending Skeletest.
+--
+-- Use 'defaultHooks' instead of using v'Hooks' directly, to minimize
+-- breaking changes.
 data Hooks = Hooks
-  { hookRunTest :: TestInfo -> IO TestResult -> IO TestResult
+  { hookModifyFileSpecs :: [SpecTree] -> IO [SpecTree]
+  -- ^ Modify the specs in a file
+  , hookRunTest :: TestInfo -> IO TestResult -> IO TestResult
+  -- ^ Modify how a test is executed
   }
 
 instance Semigroup Hooks where
   hooks1 <> hooks2 =
     Hooks
-      { hookRunTest = \testInfo -> hookRunTest hooks2 testInfo . hookRunTest hooks1 testInfo
+      { hookModifyFileSpecs = hookModifyFileSpecs hooks1 >=> hookModifyFileSpecs hooks2
+      , hookRunTest = \testInfo -> hookRunTest hooks2 testInfo . hookRunTest hooks1 testInfo
       }
 
 instance Monoid Hooks where
@@ -70,5 +85,6 @@
 defaultHooks :: Hooks
 defaultHooks =
   Hooks
-    { hookRunTest = \_ -> id
+    { hookModifyFileSpecs = pure
+    , hookRunTest = \_ -> id
     }
