The most annoying agent failure is not a bad edit. It is the agent announcing "all done" while the type checker is still failing.
Claude Code has a Stop hook that runs when the agent is about to finish its turn. If the hook exits with status 2, the stop is blocked and whatever the hook printed to stderr goes back to the agent as the reason. That gives you a cheap "you are not done yet" gate.
The hook
#!/usr/bin/env python3
import json, subprocess, sys
event = json.load(sys.stdin)
# Avoid an endless loop: if we already blocked once this turn, let it stop.
if event.get("stop_hook_active"):
sys.exit(0)
check = subprocess.run(
["npx", "tsc", "--noEmit", "--pretty", "false"],
capture_output=True, text=True, timeout=180,
)
if check.returncode != 0:
print("Type check still fails. Fix these before finishing:\n" + check.stdout[-1500:], file=sys.stderr)
sys.exit(2)
Wire it up
In .claude/settings.json:
{
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command", "command": "python3 .claude/hooks/stop_gate.py" } ] }
]
}
}
Notes from using it
- Keep the check fast. A full test suite on every stop gets old; a type check or a lint pass is usually enough.
- The
stop_hook_activeguard matters. Without it, a check the agent cannot fix will keep it looping. - Pair it with a
PostToolUsehook for per-edit checks and aPreToolUsedeny list for dangerous commands, and you get three small, boring guardrails instead of one long prompt.
I keep this hook, the pre-edit deny list, and a short house-rules file together in a small Claude Code ops pack: Vildanden Agent Config. The free sample is enough to try the pattern.
Top comments (0)