Build1 publisher3 min readPublished
Exit code 2 from a Stop hook keeps Claude Code working until its tests pass
Claude Code keeps working whenever a Stop hook exits with code 2, so a 20-line script can hold the agent until the project's tests pass. The gate is only as strict as the command it runs, and the posted version lets Claude stop after four consecutive blocks.
The Engineer · Build desk

What happened
- When a Stop hook exits with code 2, Claude Code refuses to end the turn and hands the hook's stderr back to Claude to act on.
- A script posted on dev.to runs the command in .claude/verify.txt on every stop and returns the last 40 lines of failing output with exit code 2.
- The script lets Claude stop anyway after four consecutive blocks, and also when no verify.txt file exists in the project.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A green gate proves only that the chosen command passed, so a verify.txt that misses the changed code lets Claude finish on tests that never touched it.
- exposure If Claude Code omits consecutive_blocks from the hook input, the posted cap never fires and a persistently failing check can hold the agent in a loop.
- cost Every stop pays for a test run, including turns where Claude only answers a question, until the hook is taught to skip a clean tree.
The author's diagnosis of the failure fits in one sentence. "It stops when the work looks done, because it has no other signal," the author wrote of Claude [3]. A Stop hook supplies that signal from the harness. It fires every time Claude tries to end its turn [1], so the model does not decide whether the check runs. It only sees the result, delivered as stderr [2].
The script is short enough to review in one sitting. It reads a command from .claude/verify.txt and lets Claude stop if the file is missing [4]. Otherwise it runs the command through the shell from the project directory, with a 300-second timeout. On failure it prints the last 40 lines of output to stderr, followed by "Fix the failures above, then finish. Do not weaken or skip tests." Then it exits 2 [5]. In the author's example, a refund test expecting 12.35 received 12.349999, and Claude fixed the rounding without anyone pasting the error back [20].
The four-block cap is good engineering. The script exits 0 once consecutive_blocks reaches 4 [9], so an agent that cannot get to green hands the turn back instead of looping. The weak point is the read. The field is fetched with a default of 0 [9], and the author warns it "may not always be there" [10]. When it is absent the count never reaches 4, and the posted script has no other way to release a turn whose check keeps returning a failure [11]. The author's backup is a per-session counter kept by the hook itself [10].
Timeouts have a similar gap. The settings entry gives the hook 330 seconds [6], 30 more than the test command gets [7]. The subprocess call has no try/except. The author notes the timeout raises an exception and can leave a dev server or test watcher running, and suggests killing the whole process tree and reporting the timeout as a failure [8]. The author's rule for crashes is that any unexpected error should exit 0 [19].
The gate checks an exit code, whatever the command behind it happens to test. "The hook is only as good as the command in verify.txt," the author wrote [12]. The command has to exit non-zero on failure, run without prompts (vitest run, or jest with --watchAll=false in some setups), and cover the feature; typecheck plus the relevant tests beats either alone, according to the post [13]. The instruction not to weaken tests is a line of text in the stderr message [5].
Windows produced the post's most useful finding. There the command runs through cmd.exe, which does not treat single quotes as quotes, so python -c 'exit(1)' evaluates a string literal and exits 0 [14]. Python is doing exactly what it was told. The author caught it when a README example "passed" a test that should have failed [14].
The post cites Anthropic's best practices for Claude Code, which list verification first [16], and says Boris Cherny, who created Claude Code, has said this feedback loop improves result quality 2-3x [17]. The post does not say how that figure was measured or on what code. For it to transfer, the check has to cover what the agent changed and be cheap enough to run on every stop. As written, the hook runs the suite even when Claude only answers a question [15]. For a 10-minute suite, the author's advice is to scope the command, as in pytest tests/billing -q [18], and to skip the run when git status is clean [15].
What to watch
- Whether Claude Code guarantees the consecutive_blocks field in Stop hook input; if it does not, a per-session counter stops being optional.
- A revised script from the author that kills the process tree on timeout and skips runs when git status is clean.
- Any published measurement behind the 2-3x quality figure the post attributes to Boris Cherny.