Guide · 2026-10-20
A stop hook that asks Claude Code and Codex for a check after the last edit
By Nikhil Sood · Delhi
Several stop hooks I looked at re-run the whole test suite every time the agent tries to finish. That is slow, it runs even when nothing changed, and it says nothing about the claim the agent is actually making. This one asks a narrower question: did a check run after the last edit without a reported failure?
If the agent edited files with its edit tools in this turn and the answer is no, the hook sends it back once: run the project's check, or end the reply with a line starting BLOCKED: and the reason. It is free, MIT licensed, one Python file, standard library only, and it runs in both Claude Code and Codex, with one difference on Codex explained below.
Why "after the last edit"
One false "done" is easy to miss: a test ran, passed, and then the agent kept editing. The green result was real; it just described an earlier version of the code. A hook that only asks "did tests run in this session?" passes that case. Ordering is the whole point.
Why it doesn't read the transcript
Both agents hand the Stop hook a transcript_path, and it's tempting to parse it. Don't. Claude Code's hooks reference says the transcript "may not yet include the current turn's most recent messages when a hook fires", and the Codex hooks docs say its transcript format isn't a stable interface. So this hook keeps its own small ledger instead: a PostToolUse hook records each edit and each check as it happens, and the Stop hook reads that.
The ledger holds one short line per event and nothing else: the kind (turn started, edit, check or block), a hashed turn ID, and for a check its family and result (passed, failed or unknown). No commands, no file paths, no output. It is a JSONL file under ~/.nueton-receipts/stop-check, or the plugin's data folder, that you can open.
Install
Claude Code, as a plugin (Claude Code 2.1.275 or later):
/plugin install nueton-stop-check --marketplace Kloudedge-apex/receipts-collector
If that shortcut isn't available, run /plugin marketplace add Kloudedge-apex/receipts-collector and then /plugin install nueton-stop-check@nueton.
Claude Code or Codex, by hand. Download the script, read it, then add the hook entries from the setup instructions to ~/.claude/settings.json or ~/.codex/hooks.json:
mkdir -p ~/.nueton-receipts
curl -fsSL https://raw.githubusercontent.com/Kloudedge-apex/receipts-collector/main/plugins/stop-check/hooks/receipts_stop_check.py -o ~/.nueton-receipts/receipts_stop_check.py
Codex skips new or changed hooks until you trust them, and under codex exec it skips them without a warning. Trust them once in the interactive /hooks screen, or pass --dangerously-bypass-hook-trust for a single run. Hooks in a project's .codex/hooks.json load only when that project is trusted.
How it decides
The agent may stop when any of these is true:
No edit through the agent's edit tools was recorded in this turn.
The most recent check after the last edit passed, or its result can't be known: on Codex (see below), or when a pipe,
;,||or a command on a following line hides the exit code.Its final message has a line starting with
BLOCKED.The hook already pushed back once this turn, or the agent is already continuing because of a stop hook.
You're in plan mode, or you've switched the hook off.
Otherwise it blocks once, with one of two messages: no check has run since the last edit, or a check you ran after the last edit failed.
The Codex difference. Codex 0.163.0 doesn't pass a shell command's exit code to hooks, only its output. So on Codex the hook can insist that a check ran after the last edit, but normally can't tell a passing run from a failing one. In Claude Code it sees both.
A check is a shell command in which some segment (after cd … &&, and after runners like uv run, npx or pnpm exec) is a test, build, lint or typecheck tool: pytest, npm test, cargo test, ./gradlew test, go vet, tsc, mypy, ruff check, eslint, make test and the like. --version, --help, formatters and make clean don't count. A check piped into tail still counts as run, but its result is treated as unknown, because the pipe hides the exit code. A check started in the background doesn't count, even after it finishes, so run checks in the foreground. If your project's check is something else, set RECEIPTS_CHECK_REGEX, for example RECEIPTS_CHECK_REGEX='^\./scripts/validate$'.
Loops, and why it blocks only once
A stop hook that blocks forever traps the agent. Both agents tell the hook when they are already continuing because of it (stop_hook_active), and this hook always lets that stop through. The hook also keeps its own rule: at most one push-back per turn. If anything about the input looks wrong, it lets the agent stop. A broken hook should never be the reason your agent can't finish.
What it won't catch
A check that passes but proves nothing. A green suite that doesn't cover the change still counts as green.
Work outside the session's own tools: a check you run by hand, a deploy on another machine.
On Codex, usually whether a check passed. Codex 0.163.0 doesn't pass the exit code, so the hook normally knows only that one ran.
Edits made through shell commands rather than the agent's edit tools:
sed -i, a redirect into a file, a code generator, or on some Codex setups a shellapply_patchheredoc. Hosted tools such as web search fire no hooks either. The hook doesn't see those edits, so they don't trigger it.Cursor isn't supported.
At most it shows that a recognized check ran after the last edit and, in Claude Code, that it passed. It doesn't prove the work is right. That still needs you.
How often did this happen before?
The hook pushes back when a turn edits files and ends without a check after the last edit; it doesn't read the agent's claims. The free collector reads your local Claude Code and Codex logs and shows how often "done" held up: every completion claim marked Confirmed, Contradicted or Unverified from the commands that ran in the same turn. On my own 30 days, after an AI pass separated real claims from goals and plans, the logs could confirm 53 of 385.
Made by Nueton. Not affiliated with Anthropic or OpenAI.