DEV Community

Vildanden
Vildanden

Posted on

Stop Claude Code from saying "done" while the build is still red, with a Stop hook

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)
Enter fullscreen mode Exit fullscreen mode

Wire it up

In .claude/settings.json:

{
  "hooks": {
    "Stop": [
      { "hooks": [ { "type": "command", "command": "python3 .claude/hooks/stop_gate.py" } ] }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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_active guard matters. Without it, a check the agent cannot fix will keep it looping.
  • Pair it with a PostToolUse hook for per-edit checks and a PreToolUse deny 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 (1)

Collapse
 
vikash_ruhil_a43b452d4a88 profile image
vikash ruhil •

The stop_hook_active guard is the bit people skip, then wonder why the agent loops forever on a missing env var.

One tweak that helped me: keep the stderr payload short and acceptance-shaped (tsc / lint failures only), not every warning in the tree. If the message is noisy, the next turn burns on the wrong line. Pairing Stop with a tiny PreToolUse deny for rm -rf / force-push has been enough day to day; I only thicken the Stop suite after the same red build escapes twice.