diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## v0.4.3
+
+Bug fixes:
+* Fixed potential issue with user hooks not running
+
+API changes:
+* Add `P.check`
+
 ## v0.4.2
 
 Bug fixes:
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -90,6 +90,8 @@
 
     Options may be specified as `-optF=<name>:<value>`. See [Options](#options).
 
+    * `-threaded` may be needed here. See [Animations aren't running on long tests](#animations-arent-running-on-long-tests)
+
 1. Add `Main.hs`:
 
     ```haskell
@@ -499,3 +501,15 @@
   [ myPlugin
   ]
 ```
+
+## Troubleshooting
+
+### Animations aren't running on long tests
+
+Normally if a test is taking a long time, Skeletest displays an animated spinner in the output. But in certain scenarios, the spinner might require `-threaded`:
+
+* Test spawns and waits on a subprocess (e.g. `callProcess`, `readProcess`)
+* Test calls C FFI code
+* Test runs CPU-bound Haskell loops that don't allocate
+
+If your test suite does any of these, add `-threaded` to `ghc-options`. In some cases (e.g. when the C FFI code is `unsafe`), you'll additionally need `-with-rtsopts=-N`.
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.4.2
+version: 0.4.3
 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
diff --git a/src/Skeletest/Assertions.hs b/src/Skeletest/Assertions.hs
--- a/src/Skeletest/Assertions.hs
+++ b/src/Skeletest/Assertions.hs
@@ -97,4 +97,4 @@
 
 failContextRef :: IORef FailContext
 failContextRef = unsafePerformIO $ newIORef []
-{-# NOINLINE failContextRef #-}
+{-# OPAQUE failContextRef #-}
diff --git a/src/Skeletest/Internal/CLI.hs b/src/Skeletest/Internal/CLI.hs
--- a/src/Skeletest/Internal/CLI.hs
+++ b/src/Skeletest/Internal/CLI.hs
@@ -472,7 +472,7 @@
 
 cliFlagStoreRef :: IORef CLIFlagStore
 cliFlagStoreRef = unsafePerformIO $ newIORef Map.empty
-{-# NOINLINE cliFlagStoreRef #-}
+{-# OPAQUE cliFlagStoreRef #-}
 
 setCliFlagStore :: CLIFlagStore -> IO ()
 setCliFlagStore = writeIORef cliFlagStoreRef
diff --git a/src/Skeletest/Internal/Fixtures.hs b/src/Skeletest/Internal/Fixtures.hs
--- a/src/Skeletest/Internal/Fixtures.hs
+++ b/src/Skeletest/Internal/Fixtures.hs
@@ -192,7 +192,7 @@
       , fileFixtures = Map.empty
       , testFixtures = Map.empty
       }
-{-# NOINLINE fixtureRegistryRef #-}
+{-# OPAQUE fixtureRegistryRef #-}
 
 modifyFixtureRegistry :: (FixtureRegistry -> (FixtureRegistry, a)) -> IO a
 modifyFixtureRegistry = atomicModifyIORef fixtureRegistryRef
diff --git a/src/Skeletest/Internal/Hooks.hs b/src/Skeletest/Internal/Hooks.hs
--- a/src/Skeletest/Internal/Hooks.hs
+++ b/src/Skeletest/Internal/Hooks.hs
@@ -100,20 +100,23 @@
 
 userHooksRef :: IORef Hooks
 userHooksRef = unsafePerformIO $ newIORef defaultHooks
+{-# OPAQUE userHooksRef #-}
 
 setUserHooks :: Hooks -> IO ()
 setUserHooks = writeIORef userHooksRef
 
 userHooks :: UserHooks
-userHooks = unsafePerformIO $ UserHooks <$> readIORef userHooksRef
+userHooks = UserHooks $ readIORef userHooksRef
 
-newtype UserHooks = UserHooks Hooks
+newtype UserHooks = UserHooks (IO Hooks)
 
 instance
   (HasField field Hooks (Hook ctx inp out)) =>
   HasField field UserHooks (ctx -> inp -> (inp -> IO out) -> IO out)
   where
-  getField (UserHooks hooks) = runHook (getField @field hooks)
+  getField (UserHooks getHooks) ctx inp f = do
+    hooks <- getHooks
+    runHook (getField @field hooks) ctx inp f
 
 {----- modifySpecRegistry -----}
 
diff --git a/src/Skeletest/Internal/Paths.hs b/src/Skeletest/Internal/Paths.hs
--- a/src/Skeletest/Internal/Paths.hs
+++ b/src/Skeletest/Internal/Paths.hs
@@ -18,7 +18,7 @@
 
 originalDirectoryRef :: IORef FilePath
 originalDirectoryRef = unsafePerformIO $ newIORef (invariantViolation "Original directory not set")
-{-# NOINLINE originalDirectoryRef #-}
+{-# OPAQUE originalDirectoryRef #-}
 
 data TestRoot = TestRootBuildDir | TestRootCWD | TestRoot FilePath
 
diff --git a/src/Skeletest/Internal/Predicate.hs b/src/Skeletest/Internal/Predicate.hs
--- a/src/Skeletest/Internal/Predicate.hs
+++ b/src/Skeletest/Internal/Predicate.hs
@@ -20,6 +20,8 @@
   anything,
   anythingDeep,
   anyThunk,
+  check,
+  checkImpl,
 
   -- * Ord
   eq,
@@ -249,6 +251,39 @@
     , predicateDisp = "any thunk"
     , predicateDispNeg = "not any thunk"
     }
+
+-- | A predicate that matches any value matching the given function.
+--
+-- Equivalent to @f P.>>> P.eq True@, but with nicer error messages.
+--
+-- @since 0.4.3
+check :: forall a m. (Monad m) => (a -> Bool) -> Predicate m a
+check =
+  -- Will be replaced with checkImpl by plugin
+  invariantViolation "P.check was not replaced"
+
+-- | Implementation of 'check'.
+-- Should not be used directly, generated by the plugin.
+checkImpl :: forall a m. (Monad m) => String -> (a -> Bool) -> Predicate m a
+checkImpl funcDisplay func =
+  Predicate
+    { predicateFunc = \actual -> do
+        let success = func actual
+        pure
+          PredicateFuncResult
+            { predicateSuccess = success
+            , predicateExplain =
+                if success
+                  then render actual <> " " <> disp
+                  else render actual <> " " <> dispNeg
+            , predicateShowFailCtx = noCtx
+            }
+    , predicateDisp = disp
+    , predicateDispNeg = dispNeg
+    }
+ where
+  disp = "matches " <> Text.pack funcDisplay
+  dispNeg = "does not match " <> Text.pack funcDisplay
 
 {----- Ord -----}
 
diff --git a/src/Skeletest/Internal/PreprocessorPlugin.hs b/src/Skeletest/Internal/PreprocessorPlugin.hs
--- a/src/Skeletest/Internal/PreprocessorPlugin.hs
+++ b/src/Skeletest/Internal/PreprocessorPlugin.hs
@@ -103,6 +103,7 @@
 transformTestModule ctx =
   foldl' (.) id $
     [ replaceConMatch ctx
+    , replaceCheck ctx
     , replaceIsoChecker ctx
     ]
 
@@ -221,6 +222,43 @@
   mkNamesList = mkHList $ \name -> hsExprApps (hsExprCon $ hsName 'Const) [hsExprLitString $ getHsName name]
   mkValsList = mkHList $ \val -> hsExprApps (hsExprVar $ hsName 'pure) [hsExprVar val]
   mkPredList = mkHList id
+
+-- | Replace all uses of P.check with P.checkImpl. See P.check.
+--
+-- P.check (isValid . normalize)
+-- ====>
+-- P.checkImpl "(isValid . normalize)" (isValid . normalize)
+replaceCheck :: Ctx -> HsExpr GhcRn -> HsExpr GhcRn
+replaceCheck ctx e =
+  case getExpr e of
+    -- Matches:
+    --   P.check isValid
+    --   P.check (isValid . normalize)
+    HsExprApps (getExpr -> HsExprVar name) [arg]
+      | isCheck name ->
+          convertCheck arg
+    -- Matches:
+    --   P.check $ isValid . normalize
+    HsExprOp (getExpr -> HsExprVar name) (getExpr -> HsExprVar dollar) arg
+      | ctx.matchesName (hsName '($)) dollar
+      , isCheck name ->
+          convertCheck arg
+    -- Check if P.check is by itself
+    HsExprVar name
+      | isCheck name ->
+          skeletestPluginError (getLoc e) "P.check must be applied to a constructor"
+    -- Check if P.check is being applied to more than one argument
+    HsExprApps (getExpr -> HsExprVar name) (_ : _ : _)
+      | isCheck name ->
+          skeletestPluginError (getLoc e) "P.check must be applied to exactly one argument"
+    _ -> e
+ where
+  isCheck = ctx.matchesName (hsName 'P.check)
+  convertCheck arg =
+    hsExprApps (hsExprVar $ hsName 'P.checkImpl) $
+      [ hsExprLitString $ renderHsExpr arg
+      , arg
+      ]
 
 -- | Replace all uses of P.=== with inlined IsoChecker value, with
 -- function name filled in.
diff --git a/src/Skeletest/Internal/Snapshot.hs b/src/Skeletest/Internal/Snapshot.hs
--- a/src/Skeletest/Internal/Snapshot.hs
+++ b/src/Skeletest/Internal/Snapshot.hs
@@ -237,7 +237,7 @@
       , numSnapshotsUpdated = 0
       , numSnapshotFilesCleanedUp = 0
       }
-{-# NOINLINE snapshotInfoStoreRef #-}
+{-# OPAQUE snapshotInfoStoreRef #-}
 
 getTestIds :: Spec -> [SnapshotTestId]
 getTestIds = concatMap (go Seq.empty) . getSpecTrees
diff --git a/src/Skeletest/Internal/Snapshot/Renderer.hs b/src/Skeletest/Internal/Snapshot/Renderer.hs
--- a/src/Skeletest/Internal/Snapshot/Renderer.hs
+++ b/src/Skeletest/Internal/Snapshot/Renderer.hs
@@ -57,7 +57,7 @@
 
 snapshotRenderersRef :: IORef [SnapshotRenderer]
 snapshotRenderersRef = unsafePerformIO $ newIORef []
-{-# NOINLINE snapshotRenderersRef #-}
+{-# OPAQUE snapshotRenderersRef #-}
 
 setSnapshotRenderers :: [SnapshotRenderer] -> IO ()
 setSnapshotRenderers = writeIORef snapshotRenderersRef
diff --git a/src/Skeletest/Internal/TestInfo.hs b/src/Skeletest/Internal/TestInfo.hs
--- a/src/Skeletest/Internal/TestInfo.hs
+++ b/src/Skeletest/Internal/TestInfo.hs
@@ -40,7 +40,7 @@
 
 testInfoMapRef :: IORef TestInfoMap
 testInfoMapRef = unsafePerformIO $ newIORef Map.empty
-{-# NOINLINE testInfoMapRef #-}
+{-# OPAQUE testInfoMapRef #-}
 
 withTestInfo :: (MonadUnliftIO m) => TestInfo -> m a -> m a
 withTestInfo info m = do
diff --git a/src/Skeletest/Internal/Utils/Term.hs b/src/Skeletest/Internal/Utils/Term.hs
--- a/src/Skeletest/Internal/Utils/Term.hs
+++ b/src/Skeletest/Internal/Utils/Term.hs
@@ -68,7 +68,7 @@
     supportsANSI <- ANSI.hSupportsANSI handle
     let lastInPlaceOutput = Nothing
     Handle <$> newMVar Handle'{..}
-{-# NOINLINE globalTermData #-}
+{-# OPAQUE globalTermData #-}
 
 init :: IO ()
 init = do
diff --git a/src/Skeletest/Predicate.hs b/src/Skeletest/Predicate.hs
--- a/src/Skeletest/Predicate.hs
+++ b/src/Skeletest/Predicate.hs
@@ -5,6 +5,7 @@
   anything,
   anythingDeep,
   anyThunk,
+  check,
 
   -- * Ord
   eq,
diff --git a/test/Skeletest/PredicateSpec.hs b/test/Skeletest/PredicateSpec.hs
--- a/test/Skeletest/PredicateSpec.hs
+++ b/test/Skeletest/PredicateSpec.hs
@@ -58,6 +58,15 @@
       it "ignores nested bottom" $ do
         Just bottom `shouldSatisfy` P.anyThunk
 
+    describe "check" $ do
+      it "checks using callback" $ do
+        1 `shouldSatisfy` P.check odd
+        1 `shouldNotSatisfy` P.check even
+
+      it "shows helpful failure messages" $ do
+        snapshotFailure (P.check (odd . (+ 1))) 1
+        snapshotFailure (P.not $ P.check (odd . (+ 1))) 2
+
   describe "Ord" $ do
     describe "eq" $ do
       it "checks equality" $ do
diff --git a/test/Skeletest/__snapshots__/PredicateSpec.snap.md b/test/Skeletest/__snapshots__/PredicateSpec.snap.md
--- a/test/Skeletest/__snapshots__/PredicateSpec.snap.md
+++ b/test/Skeletest/__snapshots__/PredicateSpec.snap.md
@@ -1,5 +1,21 @@
 # test/Skeletest/PredicateSpec.hs
 
+## General ≫ check ≫ shows helpful failure messages
+
+```
+1 does not match (odd . (+ 1))
+```
+
+```
+2 matches (odd . (+ 1))
+
+Expected:
+  does not match (odd . (+ 1))
+
+Got:
+  2
+```
+
 ## Ord ≫ eq ≫ shows helpful failure messages
 
 ```
