"Done" should mean done
The most frustrating agent failure is premature success: "All done! Everything works!" when the build is red. A Stop hook fixes that. It runs when Claude tries to end its turn, and if your check fails, it blocks the stop and hands Claude the failure output to fix.
The one-line gate
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "pnpm tsc --noEmit 1>&2 || exit 2" }] }
]
}
}How it works:
- The typecheck output goes to stderr (
1>&2), which is what Claude sees when the hook blocks. - If the typecheck fails,
exit 2blocks the stop. - Claude reads the errors, fixes them and tries to stop again, until the check passes.
Picking good gate checks
| Good gate | Why |
|---|---|
| Typecheck | Fast, deterministic, and catches a whole class of mistakes |
| Lint (changed files) | Fast, and enforces conventions |
| Unit tests for affected packages | Real behavioral evidence |
| Risky gate | Why |
|---|---|
| A 20-minute full e2e suite | Every stop becomes painfully slow |
| Flaky tests | Random blocks confuse Claude and waste turns |
| Anything that needs network or secrets | Fails for reasons Claude can't fix |
Run checks only on what changed, like pnpm test --changed or ruff check $(git diff --name-only). A gate that takes 10 seconds will be kept, and one that takes 10 minutes will be disabled.
Built-in loop protection
A Stop hook that can never pass could loop forever, so Claude Code caps consecutive blocked stops (8 by default). Still, design checks Claude can satisfy, and make the error output clear.
Judgment calls: prompt and agent hooks
Some checks aren't a command. "Did Claude actually address every item in the request?" For these, a prompt hook asks a model to evaluate, and an (experimental) agent hook can use tools to investigate:
{ "type": "prompt", "prompt": "Check whether the assistant ran the tests and reported their real output. If not, block with a reason." }Use command gates for hard facts and prompt gates for softer criteria.
- ⋮⋮Claude finishes its work and tries to stop
- ⋮⋮The Stop hook runs the typecheck
- ⋮⋮The check fails, so the hook exits 2 with errors on stderr
- ⋮⋮Claude reads the errors and fixes them
- ⋮⋮Claude tries to stop again, the check passes, and the turn ends
Check your understanding
When does a Stop hook run?
In pnpm tsc --noEmit 1>&2 || exit 2, why redirect to stderr?
Which makes the WORST gate?
What kind of hook evaluates soft criteria with a model?