The Stop Hook That Won’t Let Claude Lie to You
How to make Claude prove the work is done before it claims to be done.
Claude told me the work was done. The tests passed. Everything green.
It wasn’t true.
I’d asked Claude Code to fix a failing end-to-end test (e2e) and ship the feature. The final response said “all tests pass, the feature is complete.” I ran the test suite myself. One failure. Claude hadn’t even run all the tests before claiming victory.
That was the moment I stopped trusting agents to grade their own work.
The Lie Problem
Agents falsely claim completion for one main reason. It isn’t context exhaustion or a bug. It is training.
Modern large language models (LLMs) use reinforcement learning from human feedback (RLHF). RLHF rewards responses that sound clear and confident. The model learns that saying “done” lands well with humans. Whether the work is actually done is a secondary concern.
This is the same dynamic the research community calls sycophancy. The model aligns with what the user wants to hear rather than what is true. In a chat session you might catch it. In an autonomous loop you usually don’t.
That’s the dangerous part.
In a loop, a falsely-claimed completion becomes the seed of the next turn’s context. The lie compounds. You wake up with a pull request (PR) that says “all tests passing” stamped on a branch where nothing was actually run.
You’ve built on a mistake, and the agent won’t flag it. If it did, that would mean admitting the last turn was wrong.
The fix is not to make the agent more honest. You can’t train your way out of this from a settings.json file. The fix is to externally verify the output before the agent is allowed to declare victory.
Boris Cherny, who created Claude Code at Anthropic, said in January: “For great results with Claude Code, it’s key to let Claude verify its work. If Claude has that feedback loop, it will 2-3x the quality of the final result.”
He’s right. And the cleanest place to insert that feedback loop is a Stop hook.
What the Stop Hook Actually Does
The Stop event happens right after Claude finishes responding. It occurs before the turn officially ends. A Stop hook is just a shell command, or now a prompt, that the harness runs at that moment. The hook gets a vote on whether Claude is actually allowed to stop.
That’s the entire mechanism.
The hook returns one of two answers. A command hook exits 0 with no output, or a prompt hook returns {"ok": true}, and Claude stops normally. If the hook returns decision: "block" with a reason, or a prompt hook returns {"ok": false, "reason": "..."}, Claude does not stop. The harness sends the reason string back to Claude as the next instruction. Then, the loop keeps going.
The shape is small enough to memorize. Here’s the JSON that a command hook writes to stdout to continue:
{
"decision": "block",
"reason": "Tests must pass before proceeding"
}Two fields. The decision field must be the literal string "block". The reason field is what Claude reads next, so write it like an instruction, not a complaint.
This is a completion gate. It sits at the end of the turn and asks one question. Did the agent actually do what it claimed?
You get the final say.
Layering a Verifier Model
The Stop hook itself is dumb plumbing. The interesting part is what runs inside it.
The naive version is a shell script. You write a stop-verifier.sh, it reads the transcript, it makes a judgment, it emits the JSON. That works. But you end up writing prompt-engineering logic in bash, which is nobody’s idea of a good time.
Claude Code now ships the smarter version as a first-class feature. You can create a Stop hook with type: "prompt". Then, the harness sends your prompt and the hook input directly to a Claude model. The default model is Haiku. No shell script. No glue code.
You write the verifier prompt in your settings.json and Claude Code handles the rest.
This is what that looks like in .claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}
Save it, restart, and Haiku is now the judge at every Stop event.
Why Haiku and not Sonnet? Two reasons.
First, separation of concerns. You do not want the same model both writing the code and grading the code. I know Opus is always preferred, but many plugins have use sonnet as well to save on tokens and cost. That collapses the value of the second opinion.
Second, the cost math. Haiku 4.5 costs $1 per million input tokens and $5 for output. It runs over twice as fast as the previous Sonnet generation. Plus, it scores 73.3% on the Software Engineering Bench (SWE-bench) Verified. It was released October 15, 2025 specifically for this kind of workhorse role.
A tight verifier prompt is around 500 input tokens with a 50-token JSON response. At those prices, that’s a fraction of a cent per Stop event. You can fire it every turn without thinking about it.
Claude Code supports type: "agent" hooks. These are for verification that needs running commands, not just checking if tests seem complete. This creates a subagent with tool access. It can run npm test and read the result before voting. Agent hooks are still marked as experimental. So, for production workflows, use command or prompt hooks instead.
The Pattern in Practice
This is what the loop looks like when it’s wired up.
Claude finishes responding and says “all tests pass.” The Stop event fires. The prompt-type hook spins up Haiku with your verifier prompt and the recent transcript.
Haiku reads the transcript and notices Claude never actually called the test runner. It returns {"ok": false, "reason": "You claimed tests pass but never executed npm test. Run npm test and report the actual results."}.
Claude does not get to stop. That reason string lands in its context as its next instruction. Claude runs npm test, sees the failures, fixes them, and tries to stop again.
Haiku looks at the new transcript, sees a real test run with green output, and returns {"ok": true}. Claude actually stops.
The loop self-corrected. You did nothing.
For the deterministic version, you can run a test command directly. Use a command hook with this script:
#!/bin/bash
INPUT=$(cat)
# Infinite-loop guard
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if npm test > /dev/null 2>&1; then
exit 0
else
jq -n '{decision: "block", reason: "npm test exited non-zero. Read the failures and fix them before completing."}'
exit 0
fi
That stop_hook_active check at the top is not optional. More on that in a second.
The doer is Sonnet or Opus. The judge is Haiku. The harness referees. Your agent stops claiming victory it hasn’t earned. It can’t stop until the verifier says so.
One Caveat
Stop hooks can deadlock. The Claude Code docs have a troubleshooting section called “Stop hook runs forever.”
The mechanism is simple. If your hook always returns the decision "block", Claude will keep looping forever. Anthropic’s fix is a JSON input field called stop_hook_active. When the hook fires a second time because of a prior block, that field is true. You read it. You exit 0. Claude is allowed to stop.
This is the official snippet, verbatim from the docs:
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Allow Claude to stop
fi
# ... rest of your hook logic
The flag does not prevent loops automatically. You have to check it. If your script ignores it, you do get an infinite loop.
There are two more traps worth knowing.
Keep the verifier prompt narrow. “Is the work complete?” is too vague and produces false positives. “Did the requested files change AND did npm test exit zero?” is verifiable.
And do not use the same model as both doer and judge. The whole point is a second opinion, and a second opinion from the same model is just the first opinion again.
One more thing, and this is important. A Stop hook catches premature completion. It does not catch correctness bugs. A verifier that asks “did Claude run the tests?” cannot detect that Claude wrote broken tests. This pattern is a completion gate, not a correctness gate. Treat it that way.
The Bigger Picture
The lesson is small but it generalizes. Agents shouldn’t be trusted to grade themselves.
The Stop hook is just a clean place in the Claude Code architecture to insert a second opinion. One that runs on a cheaper model. One that is trained to be skeptical rather than agreeable. One that you control with a single block of JSON.
This is where AI engineering is actually heading. Not bigger models. Better scaffolding around them.
The models are becoming commodities. The scaffolding is becoming the product.
Wire up a Stop hook this week. Use a type: "prompt" config with the Haiku default. Give it a narrow, verifiable question. Watch what happens the next time Claude tries to tell you the work is done.
It won’t get to lie to you again.
Until next time,
Cheers friends,
Eric Roby
Find me online:









