diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# [0.2.0.0] -- 2025-05-02
+
+[0.2.0.0]: https://github.com/GaloisInc/oughta/releases/tag/v0.2.0.0
+
+- Add hooks, `Lua` actions that run before or after Lua programs. To upgrade
+  from v0.1, just add `defaultHooks` as the first argument to `check{,'}`.
+- Add the `reset` function to the Lua API.
+
 # [0.1.1.0] -- 2025-04-22
 
 [0.1.1.0]: https://github.com/GaloisInc/oughta/releases/tag/v0.1.1.0
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -21,8 +21,11 @@
 check "world"
 ```
 
-Oughta draws inspiration from LLVM's [FileCheck] and Rust's [compiletest].
+Oughta draws inspiration from [DejaGNU] (which is used to test [GCC]), LLVM's
+[FileCheck], and Rust's [compiletest].
 
+[DejaGNU]: https://www.gnu.org/software/dejagnu/
+[GCC]: https://gcc.gnu.org/
 [FileCheck]: https://llvm.org/docs/CommandGuide/FileCheck.html
 [compiletest]: https://rustc-dev-guide.rust-lang.org/tests/compiletest.html
 
@@ -193,12 +196,14 @@
 Checking functions:
 
 - `check(s: String)`: Find `s` in `text`. Seek to after the end of `s`. Like
-  LLVM FileCheck's `CHECK`.
+  LLVM FileCheck's `CHECK`, or [Expect's][expect] `expect`.
 - `checkln(s: String)`: `checkln(s)` is equivalent to `check(s .. "\n")`.
 - `here(s: String)`: Check that `text` beings with `s`. Seek to after the end
   of `s`.
 - `hereln(String)`: `hereln(s)` is equivalent to `here(s .. "\n")`.
 
+[expect]: https://core.tcl-lang.org/expect/index
+
 Other utilities:
 
 - `col() -> Int`: Get the current line number of `text` in the output.
@@ -212,6 +217,9 @@
 - `fail()`: Fail to match at this point in `text`.
 - `match(n: Integer)`: Consume `n` bytes of `text`, treating them as a match.
 - `seek(n: Integer)`: Seek forward `n` bytes in `text`.
+- `reset(name: String, s: String)`: Set `text` to `s` and reset the program
+  state (e.g., location tracking). Treat `name` as the file name for `s` in
+  user-facing output.
 
 ## Motivation
 
diff --git a/oughta.cabal b/oughta.cabal
--- a/oughta.cabal
+++ b/oughta.cabal
@@ -1,6 +1,6 @@
-cabal-version: 2.4
+cabal-version: 3.0
 name: oughta
-version: 0.1.1.0
+version: 0.2.0.0
 author: Galois, Inc.
 maintainer: grease@galois.com
 copyright: Galois, Inc. 2025
@@ -26,7 +26,7 @@
 
 common shared
   default-language: GHC2021
-  build-depends: base >= 4.17 && < 5
+  build-depends: base ^>= { 4.17, 4.18, 4.19, 4.20, 4.21 },
 
   -- Specifying -Wall and -Werror can cause the project to fail to build on
   -- newer versions of GHC simply due to new warnings being added to -Wall. To
@@ -135,16 +135,17 @@
   build-depends:
     bytestring,
     containers,
-    exceptions >= 0.10 && < 0.11,
-    file-embed >= 0.0.16 && < 0.1,
-    hslua >= 2.3 && < 2.4,
-    text,
+    exceptions ^>= 0.10,
+    file-embed ^>= 0.0.15,
+    hslua ^>= 2.3,
+    text ^>= { 2, 2.1 },
 
   exposed-modules:
     Oughta
   other-modules:
     Oughta.Exception
     Oughta.Extract
+    Oughta.Hooks
     Oughta.Lua
     Oughta.LuaApi
     Oughta.Pos
@@ -157,7 +158,7 @@
   hs-source-dirs: test
   main-is: Test.hs
   build-depends:
-    oughta
+    oughta >= 0.2.0.0,
   build-depends:
     bytestring,
     directory,
diff --git a/src/Oughta.hs b/src/Oughta.hs
--- a/src/Oughta.hs
+++ b/src/Oughta.hs
@@ -21,12 +21,16 @@
   , Loc(..)
   , Pos(..)
   , OP.Span(..)
+    -- * Hooks
+  , OH.Hooks(..)
+  , OH.defaultHooks
   ) where
 
 import Control.Exception qualified as X
 import Data.ByteString (ByteString)
 import Oughta.Extract (LuaProgram)
 import Oughta.Extract qualified as OE
+import Oughta.Hooks qualified as OH
 import Oughta.LuaApi qualified as FCLA
 import Oughta.Pos (Loc(..), Pos(..))
 import Oughta.Pos qualified as OP
@@ -39,19 +43,21 @@
 
 -- | Check some program output against a Oughta Lua program.
 check ::
+  OH.Hooks ->
   LuaProgram ->
   Output ->
   IO OR.Result
-check prog (Output out) = FCLA.check prog out
+check hooks prog (Output out) = FCLA.check hooks prog out
 
 -- | Like 'check', but throws an exception on failure.
 check' ::
   HasCallStack =>
+  OH.Hooks ->
   LuaProgram ->
   Output ->
   IO ()
-check' prog out = do
-  OR.Result r <- check prog out
+check' hooks prog out = do
+  OR.Result r <- check hooks prog out
   case r of
     Left f -> X.throwIO f
     Right {} -> pure ()
diff --git a/src/Oughta/Hooks.hs b/src/Oughta/Hooks.hs
new file mode 100644
--- /dev/null
+++ b/src/Oughta/Hooks.hs
@@ -0,0 +1,21 @@
+module Oughta.Hooks
+  ( Hooks(..)
+  , defaultHooks
+  ) where
+
+import HsLua (Lua)
+
+data Hooks
+  = Hooks
+    { -- | Hook that runs before execution of the Lua program
+      preHook :: Lua ()
+      -- | Hook that runs after successful execution of the Lua program
+    , postHook :: Lua ()
+    }
+
+defaultHooks :: Hooks
+defaultHooks =
+  Hooks
+  { preHook = pure ()
+  , postHook = pure ()
+  }
diff --git a/src/Oughta/LuaApi.hs b/src/Oughta/LuaApi.hs
--- a/src/Oughta/LuaApi.hs
+++ b/src/Oughta/LuaApi.hs
@@ -17,6 +17,7 @@
 import Oughta.Exception (Exception)
 import Oughta.Exception qualified as OE
 import Oughta.Extract (LuaProgram, SourceMap, lookupSourceMap, programText, sourceMap, sourceMapFile)
+import Oughta.Hooks qualified as OH
 import Oughta.Lua qualified as OL
 import Oughta.Pos qualified as OP
 import Oughta.Result (Progress, Result)
@@ -85,6 +86,13 @@
           }
     pure (OR.updateProgress m p)
 
+-- | Implementation of @reset@. Not exported.
+reset :: IORef Progress -> String -> ByteString -> Lua.LuaE Exception ()
+reset stateRef name txt = do
+  setText txt
+  let p0 = OR.newProgress name txt
+  liftIO (IORef.writeIORef stateRef p0)
+
 -- | Implementation of @seek@. Not exported.
 seek :: IORef Progress -> Int -> Lua.LuaE Exception ()
 seek stateRef chars =
@@ -123,13 +131,14 @@
 
 -- | Load user and Oughta Lua code. Helper, not exported.
 luaSetup ::
+  OH.Hooks ->
   IORef Progress ->
   -- | User code
   LuaProgram ->
   -- | Initial content of @text@ global
   ByteString ->
   Lua.LuaE Exception ()
-luaSetup stateRef prog txt = do
+luaSetup hooks stateRef prog txt = do
   Lua.openlibs
   setText txt
 
@@ -150,6 +159,9 @@
   Lua.pushHaskellFunction (Lua.toHaskellFunction (match sm stateRef))
   Lua.setglobal (Lua.Name "match")
 
+  Lua.pushHaskellFunction (Lua.toHaskellFunction (reset stateRef))
+  Lua.setglobal (Lua.Name "reset")
+
   Lua.pushHaskellFunction (Lua.toHaskellFunction (seek stateRef))
   Lua.setglobal (Lua.Name "seek")
 
@@ -159,20 +171,25 @@
   _ <- Lua.loadbuffer OL.luaCode (Lua.Name "oughta.lua")
   Lua.call 0 0
 
+  Lua.changeErrorType (OH.preHook hooks)
+
   let nm = Lua.Name (Text.encodeUtf8 (sourceMapFile sm))
   _ <- Lua.loadbuffer (Text.encodeUtf8 (programText prog)) nm
   Lua.call 0 0
 
+  Lua.changeErrorType (OH.postHook hooks)
+
 -- | Check some text against a Lua program using the API.
 check ::
+  OH.Hooks ->
   LuaProgram ->
   -- | Text to check
   ByteString ->
   IO Result
-check prog txt = do
+check hooks prog txt = do
   let p0 = OR.newProgress "<out>" txt
   stateRef <- IORef.newIORef p0
-  result <- Lua.run (Lua.try (luaSetup stateRef prog txt))
+  result <- Lua.run (Lua.try (luaSetup hooks stateRef prog txt))
   case result of
     Left (OE.LuaException e) -> X.throwIO e
     Left (OE.Failure noMatch) ->
diff --git a/test-data/fail/reset.txt b/test-data/fail/reset.txt
new file mode 100644
--- /dev/null
+++ b/test-data/fail/reset.txt
@@ -0,0 +1,4 @@
+# reset("<out>", "bar")
+# check "foo"
+foo
+; check "❌"
diff --git a/test-data/pass/reset.txt b/test-data/pass/reset.txt
new file mode 100644
--- /dev/null
+++ b/test-data/pass/reset.txt
@@ -0,0 +1,4 @@
+# reset("<out>", "bar")
+# check "bar"
+foo
+; match_on_line(1)
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -51,14 +51,14 @@
 
   let prog0 = Ota.fromLineComments file comment content
   let prog = Ota.addPrefix (Text.decodeUtf8Lenient prelude) prog0
-  result <- Ota.check prog (clearComments content)
+  result <- Ota.check Ota.defaultHooks prog (clearComments content)
   TTH.assertBool file (not (Ota.resultNull result))
 
   let prog0' = Ota.fromLineComments file comment' content
   let prog' = Ota.addPrefix (Text.decodeUtf8Lenient prelude) prog0'
   let output'@(Ota.Output out) = clearComments (Ota.printResult result)
   BS.writeFile (FilePath.replaceExtension file "out") out
-  Ota.check' prog' output'
+  Ota.check' Ota.defaultHooks prog' output'
 
 discover :: FilePath -> IO [TT.TestTree]
 discover dir = do
