Make Codex run your tests before it says done
On this page
  1. How the Stop hook works
  2. The script
  3. The config
  4. The step that makes it do nothing: trust
  5. What happened when we ran it
  6. Make it fit your project
  7. Where Joxo fits
  8. Frequently asked questions
  9. Does Codex CLI have a Stop hook?
  10. Why doesn't my Codex hook run?
  11. How do I stop a Codex Stop hook from looping?
  12. Try it with your team

Blog / codex

Make Codex run your tests before it says done

A Codex Stop hook that runs your tests and sends Codex back to work when they fail. Tested on Codex CLI 0.164, including the trust step that skips it.

Codex says "done", you run the tests, and two of them fail. You can ask it every time to run the tests first, or you can make it impossible to finish while they fail. Codex CLI has a Stop hook for exactly this: a command that runs each time Codex is about to end its turn, and that can send it back to work with the test output as its next instruction.

We set this up in a small Python project and ran it with Codex CLI 0.164. Below is the script, the config, what happened, and the one step that makes the hook silently do nothing if you skip it.

How the Stop hook works#

When Codex finishes responding, it runs every hook registered for the Stop event and passes it a JSON object on standard input. Two fields matter here:

  • stop_hook_active is true when this turn is already a continuation caused by a Stop hook.
  • last_assistant_message is the text Codex was about to end on.

The hook answers in one of two ways. Exit with code 0 and print nothing, and Codex stops as normal. Exit with code 2 and write a reason to standard error, and Codex does not stop: it treats your reason as a new prompt and keeps going. (You can also print {"decision": "block", "reason": "..."} on standard output and exit 0. On exit 0, Codex expects JSON or nothing on standard output; plain text there is invalid for this event.)

So the hook only needs to run your tests, stay quiet when they pass, and hand Codex the failures when they don't.

The script#

Save this as .codex/stop-tests.sh in your project and make it executable (chmod +x .codex/stop-tests.sh):

bash
#!/bin/bash
# Stop hook: Codex can't finish while the tests fail.
input=$(cat)
# Already continuing because of this hook? Let it stop, so it never loops forever.
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! out=$(python3 -m unittest 2>&1); then
  echo "Tests fail. Fix them before you say you're done:" >&2
  echo "$out" | tail -20 >&2
  exit 2
fi
exit 0

Swap python3 -m unittest for your own command: npm test, pytest -q, go test ./..., cargo test. Keep the tail -20: Codex needs the failing test and the error, not five hundred lines of passing output.

The stop_hook_active check is the safety valve. Without it, a test Codex can't fix would send it round the same loop for as long as you let it run. With it, Codex gets one extra pass per turn, and then it may stop and tell you what is still wrong.

The config#

Codex reads hooks from a hooks.json file next to its config. For one project, that is .codex/hooks.json at the project root:

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "bash .codex/stop-tests.sh", "timeout": 120 }
        ]
      }
    ]
  }
}

Stop takes no matcher, so the group is just a list of handlers. Set timeout (in seconds) above the time your test suite needs. To use the hook in every project, put the same file in ~/.codex/hooks.json and point command at a script that finds the project's tests itself.

The step that makes it do nothing: trust#

Codex doesn't run a new or changed hook until you have reviewed and trusted it. It records trust against the hook's exact definition, so editing the command later puts it back up for review. Project hooks also load only when the project's .codex/ folder is trusted.

We saw this ourselves. When we ran the same job without trusting the hook, Codex edited the file, never ran our script, and finished with the tests still red. Nothing in the output said the hook was skipped.

To fix it, start Codex in the project and type /hooks. It lists every hook it found, marks the ones waiting for review, and lets you trust or disable each one. In the interactive CLI, Codex also prints a warning at startup pointing you to /hooks when something needs review; codex exec printed nothing in our run.

For a one-off script run, codex exec --dangerously-bypass-hook-trust runs enabled hooks without saved trust. Use it only for hooks you wrote and checked yourself.

What happened when we ran it#

The test project had one function with a bug (add returned a - b) and one test that caught it. We asked Codex for something unrelated: "Add a docstring to the add function in calc.py. Change nothing else."

  1. Codex added the docstring and tried to finish.
  2. The Stop hook ran with stop_hook_active=false, the test failed, and the hook exited 2 with the failure.
  3. Codex read the failure, changed a - b to a + b, ran the tests itself, and saw them pass.
  4. The hook ran again, this time with stop_hook_active=true, and let it stop.

The hook's own log showed exactly two runs, seven seconds apart. Codex's final message was "Fixed add to return the sum. Tests now pass." The diff had the docstring and the fix.

Note what this means: Codex changed code you didn't ask it to touch, because the hook told it the tests must pass. That is usually what you want. If it isn't, change the reason the hook gives, for example "Tests fail. Tell me which ones and why; don't change code outside the files you were asked to edit."

Make it fit your project#

  • Slow suites: run only the fast tests in the hook (pytest -q -m "not slow", npx jest --onlyChanged) and leave the full run for CI or for yourself.
  • Lint and types too: chain them: npm run lint && npx tsc --noEmit && npm test. The first failure is what Codex sees.
  • Nothing changed: if Codex only answered a question, running the tests is wasted time. Exit 0 early when git status --porcelain prints nothing.
  • Monorepos: run the tests for the package Codex touched. git diff --name-only tells you which.
  • Claude Code too: the same idea, almost the same script, works in Claude Code. See Make Claude Code run tests before it says done.

Where Joxo fits#

A check like this matters most when Codex works while you're away. With Joxo, when Codex or Claude Code stops to ask you something, the question reaches your phone and desktop, and you answer from there. You can also run Codex and Claude Code side by side on one project and see both in one place. Setup is one line pasted into your agent; for the side-by-side setup, see Claude Code and Codex in the same repo.

Frequently asked questions#

Does Codex CLI have a Stop hook?#

Yes. Codex CLI runs hooks on events including Stop, which fires when Codex is about to end its turn. Exit with code 2 and a reason on standard error, and Codex continues with that reason as its next prompt.

Why doesn't my Codex hook run?#

Most often because it isn't trusted yet. Codex skips new or changed hooks until you trust them in /hooks, and it loads project hooks only when the project's .codex/ folder is trusted. Also check that the file is valid JSON and the script is executable.

How do I stop a Codex Stop hook from looping?#

Read stop_hook_active from the hook's input and exit 0 when it is true. Codex then gets one extra pass per turn and can stop afterwards, even if the tests still fail.