Chapter 03 · Section III · 12 min read
3.3 Hooks: lifecycle events and permission gates
Hooks are the automation surface — shell commands Claude Code runs on lifecycle events. The event catalog, the matcher shape, and how PreToolUse hooks double as programmable permission gates.
Hooks let you say “whenever X happens, run this command.” Claude Code fires an event; your hook is a small shell command that reads context from env vars and either returns silently, prints something into the transcript, or blocks the underlying action.
Lifecycle events
The events you will actually use:
| Event | Fires when |
|---|---|
SessionStart | A session opens |
UserPromptSubmit | You send a message |
PreToolUse | Before a tool call runs; can block |
PostToolUse | After a tool call finishes |
Notification | Claude wants your attention (e.g. permission prompt) |
Stop | Claude finishes its response |
SubagentStop | A subagent completes |
PreCompact | Before context compaction |
Full list is version-dependent; claude hooks list-events is the source of truth.
The config shape
Hooks live in settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npm run lint --silent" }
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/bash-audit.sh" }
]
}
]
}
}
matcher— a regex matched against the tool name (or event-specific field). Omit to fire on every occurrence.command— the shell to run. Receives event context as env vars:CLAUDE_HOOK_EVENT,CLAUDE_TOOL_NAME,CLAUDE_TOOL_INPUT, and event-specific ones.
PreToolUse as a permission gate
A PreToolUse hook can approve, block, or ask, by its exit code:
- Exit 0 — proceed with the tool call.
- Exit 2 — block. Claude sees the block and adapts.
- Any other non-zero — treated as an error; call is refused.
This is powerful: you can write a policy in real code that no static allowlist could express. Example — block any Bash command that writes to /etc or contains a token:
#!/bin/bash
set -e
CMD="$CLAUDE_TOOL_INPUT_COMMAND"
if echo "$CMD" | grep -qE '(/etc/|sk-ant-api)'; then
echo "blocked: sensitive target or secret" >&2
exit 2
fi
exit 0
Common patterns
- Auto-lint on save.
PostToolUsematched onEdit|Writeruns the linter. - Log every Bash command.
PreToolUsematched onBashappends the command to a session log. - Notification bell.
Stoprunssay "done"ornotify-send. - Rate-limit expensive tools.
PreToolUseonWebSearchchecks a counter, blocks after N calls.
Check your understanding
Quick check
—