Hooks — Deterministic Enforcement
Hooks intercept the agent loop at three injection points — PreToolUse, PostToolUse, and Stop — using exit codes to block, warn, or allow. Unlike CLAUDE.md rules, hooks fire deterministically regardless of context state.
Introduction
CLAUDE.md rules are semantic — they tell Claude what to do and rely on the model reading, understanding, and following them. Hooks are mechanical — they intercept the agent loop at specific events and respond with exit codes that either allow or block the action, regardless of what Claude knows or believes.
The difference is reliability. A CLAUDE.md rule can be overwhelmed by context, misinterpreted, or simply not load if the file is too long. A hook fires on every matching tool call, every session, with zero cognitive overhead.
The Three Injection Points
PreToolUse fires before any tool runs. This is where you place guards: block destructive Bash commands, check file write contents for secrets, validate that the tool input meets your safety requirements. If the PreToolUse hook exits 2, the tool never runs — Claude must handle the block.
PostToolUse fires after a tool returns. The tool has already run; the hook cannot undo it. PostToolUse is for logging, auditing, and incremental counters. Knox's doom-loop hook is PostToolUse — it counts Edit calls per file and warns (stderr) at the threshold. The edit happened; the warning is advisory.
Stop fires when Claude wants to finish responding — when it believes the task is complete. A Stop hook that exits 2 prevents the session from ending. Knox's test gate is a Stop hook: it runs npm run test and blocks completion if tests fail. Claude cannot claim done until the gate passes.
These are the big three you'll reach for most, but they aren't the whole surface — the harness also exposes UserPromptSubmit, SessionStart/SessionEnd, SubagentStop, Notification, and PreCompact. The exit-code mechanics below apply identically; master PreToolUse/PostToolUse/Stop first and the rest follow the same pattern.
The Exit Code System
The exit code is the entire interface between your hook and the agent loop:
- Exit 0 — allow. Tool runs (PreToolUse) or session ends (Stop). No message to Claude unless you want one.
- Exit 2 — block. Tool call is prevented (PreToolUse) or session cannot end (Stop). Claude receives the stderr output as the reason for the block.
- Other non-zero — non-blocking error. The tool is not prevented, but the error is shown to the user. Use this when you want to surface a warning without stopping execution.
The doom-loop hook is PostToolUse — the edit has already completed before the hook fires. The hook exits 2 (block signal) with stderr output so Claude receives the warning as context. Because the tool already ran, the practical effect is informational: the edit went through, but Claude sees the doom-loop message and is nudged to use Write instead of a tenth Edit.
Writing Your First Hook
The template covers the three non-negotiable elements of any hook:
- Read stdin and parse it without crashing on malformed input (JSON parse errors → exit 0)
- Do your check
- Exit with the correct code
Never hang, never modify files, never make network calls. The hook runs in the critical path of every tool call.
Wiring a Hook into settings.json
The wiring step has a concrete JSON shape. Hooks live under the hooks key in .claude/settings.json, grouped by event, with a matcher that selects which tool invocations the hook fires on:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/block-main-push.sh",
"timeout": 5
}
]
}
]
}
}
Three parts to get right:
matcheris matched against the tool name."Bash"fires on every Bash call;"Write|Edit"(regex is allowed) fires on both file-writing tools; an empty or omitted matcher fires on every tool. Stop hooks take no matcher — they fire on every Stop event.commandis the script to run. It receives the tool input as JSON on stdin and signals its verdict through the exit code.timeout(seconds) caps how long the hook may run — the backstop against a hung hook stalling the agent loop.
PreToolUse, PostToolUse, and Stop are the big three, but the hook system covers more of the loop: UserPromptSubmit (inspect or enrich the prompt before Claude sees it), SessionStart, SubagentStop, PreCompact, and Notification all accept the same configuration shape. Advanced hooks can also emit structured JSON on stdout (fields like permissionDecision and additionalContext) instead of relying on the exit code alone — useful once your guards need to do more than allow/block.
Now build the most important one: the anti-main-push guard. Read the Bash command off stdin, block a push to main with exit 2, and wire it into settings.json with the shape above so it fires on every Bash call.
Knox's Production Hooks
The hooks that run in Knox's daily workflow:
Anti-main-branch guard (PreToolUse, Bash)
Reads the Bash command input, checks whether it contains git push targeting main or master, exits 2 with a stderr message. This fires on every Bash call, costs zero tokens, and has prevented dozens of accidental main-branch pushes.
Secret scan (PreToolUse, Write) When Claude writes a file, checks the content for common secret patterns (AWS key prefixes, Bearer token patterns, private key headers). Exits 2 if detected, blocking the write and surfacing the pattern that matched.
Doom-loop detector (PostToolUse, Edit) Counts Edit calls per file path in the current session. At 8 edits to the same file, writes "doom-loop threshold reached — consider Write for full file replacement" to stderr. The edit still happens; Claude gets context to self-correct.
Test gate (Stop)
Runs npm run test (for the academy) or pytest (for Python projects). If tests fail, exits 2 with the failure summary on stderr. Claude cannot end the session without addressing the failures.
When Hooks Replace CLAUDE.md Rules
The test: if you have a CLAUDE.md rule that says "never do X," ask whether you'd be devastated if X happened once. If yes, the rule belongs in a hook. CLAUDE.md is semantic guidance — it shapes behavior across hundreds of turns. A hook is physical enforcement — it prevents the action regardless.
Rules that belong in hooks:
- Never push to main
- Never write files containing detected secrets
- Never end a session without running tests
- Never run
rm -rfoutside of/tmp
Rules that belong only in CLAUDE.md (hooks cannot encode them):
- Use Supabase for auth, not custom JWT
- Follow the design system when writing UI components
- This project targets Python 3.11 — use
from __future__ import annotations
The hook layer and the CLAUDE.md layer are complementary, not competing. Mechanical rules belong in hooks; semantic knowledge belongs in CLAUDE.md.
What's Next
The next lesson goes deeper on hook architecture and security — what hooks can and cannot do, how to audit the blast radius of each hook you deploy, and the safety patterns that prevent hooks from becoming attack surfaces.