The self-healing build loop
2.12
A PostToolUse hook runs npm run build after every file edit. If it fails, the hook returns a structured block with the compiler output, and the agent fixes its own mistake.
{ "hooks": { "PostToolUse": [
{ "type": "command", "command": "node .github/hooks/scripts/build-validator.cjs", "timeout": 120 }
] } }
// on failure, print and exit 0:
{ "decision": "block",
"reason": "The project no longer builds. Fix the errors below.",
"hookSpecificOutput": { "hookEventName": "PostToolUse",
"additionalContext": "npm run build failed: Could not resolve ./BatteryWidget …" } }
TrapThere's no tool filter in the config: hooks fire for every tool call. Your script checks tool_name itself (replace_string_in_file, create_file, apply_patch…). Test it by piping fake events: echo '{"tool_name":"create_file"}' | node …
Exit codes and events
Local harness
| Exit | Meaning |
0 | Success. Stdout is parsed as JSON. |
2 | Blocking error. Stderr goes to the model. |
| other | Warning only; processing continues. |
Events: SessionStart · UserPromptSubmit · PreToolUse · PostToolUse · PreCompact · SubagentStart · SubagentStop · Stop. A Stop hook can say "not done yet" (guard it with stop_hook_active, or it loops and bills).
Make guardrails guard themselvesA PreToolUse hook that denies edits to .github/hooks/ means a confused agent can't switch off the hook that's blocking it. That's the difference between a rule and a lock.