Lesson 3.3
Claude Code Hooks
The hook events Claude Code fires, how a hook receives its input and returns a decision, and the patterns hooks are good for.
Hooks are shell commands (or prompts, or small agents) that Claude Code runs automatically at defined points in a session. Unlike instructions in CLAUDE.md, a hook always runs; the model cannot decide to skip it. That makes hooks the primary mechanism for deterministic enforcement, formatting, auditing and context injection. The exam tests the event names, the input and output contract, and the meaning of exit codes.
Events
| Event | When it fires | Can it block? |
|---|---|---|
SessionStart | A session begins or resumes | No; stdout becomes context |
UserPromptSubmit | The user submits a prompt, before Claude sees it | Yes; can also add context |
PreToolUse | Before a tool executes | Yes; can allow, deny, ask or rewrite input |
PermissionRequest | A permission prompt is about to be shown | Yes; can answer it |
PostToolUse | After a tool succeeds | Feedback only; can add context |
Notification | Claude Code sends a notification (waiting for input, etc.) | No |
Stop | The main agent is about to finish its turn | Yes; can force it to continue |
SubagentStop | A subagent finishes | Yes, for the subagent |
PreCompact | Before context compaction (manual or automatic) | No; can add instructions |
SessionEnd | The session ends | No; cleanup and logging |
Further events exist for setup, configuration changes, worktree lifecycle and multi-agent coordination; the ten above are the ones to know cold.
Configuration
Hooks live under hooks in any settings file, grouped by event, then by matcher.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh", "timeout": 30 }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [ { "type": "command", "command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"" } ]
}
],
"Stop": [
{ "hooks": [ { "type": "prompt", "prompt": "Did the agent run the tests? Answer with a decision." } ] }
]
}
}
The matcher is a regular expression matched against the tool name for tool events (Bash, Edit|Write, mcp__github__.*); an empty or missing matcher, or *, matches everything. Events without a tool (such as Stop) ignore matchers or use them for sub-types (for example the compaction trigger or the session-start source). type: "command" runs a shell command; type: "prompt" asks a model to evaluate a yes/no question against the event; newer versions add agent-style hooks that can use tools. Hooks run in parallel within an event and have a default timeout of 60 seconds. Because hooks execute arbitrary code, Claude Code snapshots them at startup and asks you to review changes made by an external editor through the /hooks menu before they take effect.
Input and output contract
A command hook receives a JSON object on stdin containing common fields (session_id, transcript_path, cwd, hook_event_name) plus event-specific ones: tool_name, tool_input and, for post-tool events, tool_response; prompt for prompt submission; trigger for compaction. Environment variables such as CLAUDE_PROJECT_DIR are also available.
The hook communicates back through its exit code and, optionally, JSON on stdout.
| Exit code | Meaning |
|---|---|
0 | Success. For UserPromptSubmit and SessionStart, stdout is added to Claude's context; for other events stdout is shown to the user only |
2 | Blocking error. stderr is fed back to Claude as the reason, and the action is prevented (tool not run, prompt not processed, stop not permitted) |
| Other non-zero | Non-blocking error. stderr is shown to the user and execution continues |
For finer control a hook exits 0 and prints JSON:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "Pushing to a protected branch",
"updatedInput": { "command": "git push origin feature-branch" }
},
"suppressOutput": true
}
Top-level fields apply to any event: continue: false halts Claude entirely with a stopReason shown to the user, and systemMessage adds a warning. Event-specific fields sit under hookSpecificOutput: permissionDecision and updatedInput for PreToolUse, additionalContext for prompt submission and post-tool events, and for Stop a top-level decision: "block" with a reason that tells Claude why it must keep working.
Patterns
- Formatting and linting after every edit with
PostToolUse, so the model never has to be reminded. - Guarding destructive commands with
PreToolUsescripts that exit 2. - Context injection at
SessionStart(current branch, open tickets) orUserPromptSubmit(inject a standards reminder for every prompt). - Completion gates with
Stophooks that block until tests pass, always with a way out. - Audit logging with
PostToolUseorSessionEndwriting to a file. - Protecting against compaction loss with
PreCompactwriting the transcript elsewhere.
Hook scripts should be fast, idempotent and quiet on success. Quote every variable derived from hook input, because tool arguments are model-generated and may contain shell metacharacters.
Exam focus
PreToolUse,UserPromptSubmit,StopandSubagentStopcan block;PostToolUse,Notification,SessionStart,SessionEndandPreCompactcannot.- Exit code 2 blocks and returns stderr to Claude; other non-zero codes are non-blocking and show stderr to the user.
- stdout of a successful
UserPromptSubmitorSessionStarthook is added to context; for other events it is only shown to the user. permissionDecision(allow/deny/ask) andupdatedInputarePreToolUseoutputs underhookSpecificOutput;decision: "block"withreasonkeeps aStopfrom completing.- Matchers are regexes on tool names; empty or
*matches all tools. - Hooks run arbitrary code: they are reviewed via
/hookswhen changed externally, and inputs must be quoted.
Check yourself
5 questions
1.Which exit code from a PreToolUse command hook blocks the tool call and feeds the hook's stderr back to Claude as the reason?
2.A team wants a formatter to run automatically every time Claude edits or writes a file. Which hook configuration is appropriate?
3.A PostToolUse hook exits with code 2 to block a dangerous Bash command, but the command has already executed. Why did blocking fail?
4.A compliance team wants hooks that developers cannot disable and plans to rely on them as the hard security gate for tool calls. How should an architect assess this plan?
5.A team wants to inject up-to-date context (for example the current sprint goal) whenever the user submits a prompt. Which hook event is appropriate, and how does the output reach Claude?
