Appearance
A Hook is an event-triggered script that fires when Claude Code does something — it has no goal of its own, only the power to allow, block, or observe.
Primitive
Hook
How a hook fires
stdin → decision → exit code. No conversation.
🤖
Agent
calls tool
→
📥
stdin (JSON)
event payload
→
🪝
Hook script
parses + decides
→
⚖
Exit code
0 / 1 / 2
The hook runs in a child process. The agent cannot see the hook's logic, cannot bypass it, cannot argue with the exit code.
Where a hook can fire
Seven events. Pick the one that matches your intent.
PreToolUse
Before any tool call. Can block the call from running.
can block
PostToolUse
After a tool call returns. Observes; can log or rewrite output.
observes
Stop
Agent about to emit Stop. Can block — verify contracts, force more work.
can block
SubagentStop
A spawned sub-agent stopping. Can block — validate before returning.
can block
UserPromptSubmit
User types a message. Can rewrite the prompt before agent sees it.
can block
PreCompact
Before /compact runs. Observes; archive context before it's summarized.
observes
Notification
Agent wants attention. Observes; route to Slack / TTS / desktop.
observes
Pick the earliest event that can see the thing you want to enforce. PreToolUse blocks; PostToolUse is too late to stop a mutation.
The only language a hook speaks
Three exit codes. Three meanings. No ambiguity.
0
allow
Green light. The tool call proceeds; the agent never knows the hook ran. Used for observation-only hooks and passed validations.
1
block silently
Red light, no message. The tool call is denied; the agent sees an error but no structured reason. Use sparingly — agents can't self-correct without a message.
2
block with message
Red light + stderr. Tool denied; the hook's stderr is passed to the agent as the block reason. The agent reads it and can retry correctly. The right choice for enforcement hooks.
Exit code 2 is the teaching signal — the agent learns from your stderr. Exit 1 is the silent deny. Exit 0 is the default.
The two kinds of hooks
Is this hook the judge, or the court reporter?
ENFORCE
Blocks when broken
The hook is the judge. It reads the payload, evaluates a contract, and returns 2 with a message if the contract fails. Agent cannot proceed.
# context-gate.sh
if ! required_reads_satisfied; then
echo "Read X before writing" >&2
exit 2
fi
AUDIT
Records what happened
The hook is the court reporter. Captures the event, appends to a log, exits 0. Never blocks. Used for context bundles, session tracking, metrics.
# context-bundle-capture.sh
echo "$INPUT" >> "$BUNDLE_FILE"
exit 0
One hook, one mode. Mixing enforce + audit in one script means a bug in the audit path can silently unblock the enforce path.
The most common bug
Hooks that do work instead of enforcement.
anti-pattern
"The hook will fix it for me"
A hook that rewrites the agent's output, generates content, or runs a build is doing work — not enforcing a contract. Hooks should allow, block, or observe. Nothing else. When a hook carries business logic, the agent has no way to know what actually ran, the hook becomes untestable in isolation, and every failure becomes a forensic investigation.
A hook that mutates is a skill with no name. Put the work in a Command, or an Agent. Keep the hook binary.
The discipline
A hook has no goal of its own.
It watches one event, enforces one contract, returns one exit code. That's the whole contract. When you need the hook to "do more," you don't need a bigger hook — you need a Command or an Agent. The value of the hook is its narrowness.