Hook Events & Handlers
Knowledge
The Most Important Hook Events
Claude Code fires events at 24 points in the workflow. Each event is an intervention point where you can enforce deterministic behavior. Here are the most important ones:
| Event | When Does It Fire? | Typical Use |
|---|---|---|
| PreToolUse | Before every tool execution | Validation, blocking dangerous actions |
| PostToolUse | After every tool execution | Auto-formatting, linting, type checks |
| SessionStart | When a new session starts | Load context, check environment |
| Stop | Claude has finished the task | Quality gates, final checks |
| UserPromptSubmit | Before a prompt is processed | Context injection, prompt enrichment |
| SubagentStart | Subagent is launched | Monitoring, configuration |
| SubagentStop | Subagent has a result | Result validation |
| TaskCreated | New task in agent teams | Coordination, logging |
| TaskCompleted | Task completed | Quality control |
| PreCompact | Before context compaction | Preserve important info |
| PostCompact | After context compaction | Restore context |
| WorktreeCreate | Git worktree created | Setup, initialization |
| WorktreeRemove | Git worktree removed | Cleanup |
| Notification | Claude is waiting for input | Desktop notification |
iPreToolUse and PostToolUse Are the Stars
These two events cover 80% of all use cases. PreToolUse blocks dangerous actions, PostToolUse ensures automatic quality assurance after every change.
The 4 Handler Types
Every Hook needs a handler -- the logic that executes when the event fires:
1. Command Handler (Default)
The most common type. Executes a shell script and receives the event context as JSON via stdin.
{
"type": "command",
"command": "npx prettier --write $TOOL_INPUT_FILE_PATH"
}
The script receives all relevant information: which tool was called, with which arguments, and what the result was. Perfect for linting, tests, formatting, and security checks.
2. HTTP Handler
Sends a POST request to an HTTP endpoint. Ideal for logging, webhooks, or integration with external systems.
{
"type": "http",
"url": "https://your-server.com/hooks/log",
"headers": { "Authorization": "Bearer $TOKEN" }
}
3. Prompt Handler
Uses a lightweight LLM (Haiku) for a single evaluation turn. Good for decisions that simulate human judgment.
{
"type": "prompt",
"prompt": "Check whether this code change breaks the existing API. Respond only with PASS or FAIL."
}
!Prompt Handlers Are Not Deterministic
The prompt handler uses an LLM -- meaning its responses can vary. For hard rules like "no force push," a command handler is better suited.
4. Agent Handler
The most powerful type: A multi-turn LLM with tool access that can execute up to 50 turns. For complex checks like "Has this change updated all tests?"
{
"type": "agent",
"prompt": "Check whether the changed files have corresponding test updates. Run the tests and report.",
"maxTurns": 10
}
Exit Codes: The Language of Hooks
Hooks communicate with Claude Code through exit codes:
| Exit Code | Meaning | Behavior |
|---|---|---|
| 0 | All OK | Continue action. stdout is added to the context |
| 2 | Block | Action is PREVENTED. stderr is returned as feedback to Claude |
| Other | Non-blocking error | Action continues. stderr is shown in verbose mode (Ctrl+O) |
This is elegant: Exit code 0 means "continue," exit code 2 means "stop." Everything else is treated as a non-critical error and proceeds.
#!/bin/bash
# Example: Block force-push
if echo "$1" | grep -q "push.*--force"; then
echo "Force-push is not allowed!" >&2
exit 2
fi
exit 0
Matchers and if Filters
Not every Hook should fire on every tool call. There are two filtering mechanisms for this:
Matcher: Regex on the Tool Name
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "npx prettier --write" }]
}
This Hook only fires on Edit and Write calls, not on Bash or Read.
if Filter: Tool + Argument
{
"matcher": "Bash",
"if": "Bash(git commit*)",
"hooks": [{ "type": "command", "command": "npm test" }]
}
The if filter is powerful: It matches not only the tool name but also the arguments. Bash(git commit*) fires only when Claude wants to execute a git commit command -- not on every Bash call.
*Combining Matcher + if
The matcher filters broadly by tool type, the if filter refines by arguments. Together, they provide precise control without overhead.
Where Hooks Are Stored
Hooks can be defined in four locations, with ascending priority:
- Global (
~/.claude/settings.json) -- Applies to all projects. Ideal for personal preferences like formatting or notifications. - Project (
.claude/settings.json) -- Applies to the entire team. Committed to the Git repository. - Local (
.claude/settings.local.json) -- Personal project overrides. NOT committed (in .gitignore). - Skill/Agent Frontmatter -- Hooks within a Skill or agent definition. Applies only to that Skill.
iTeam Hooks vs. Personal Hooks
Quality hooks like linting and tests belong in the project settings (.claude/settings.json) so the whole team benefits. Desktop notifications or personal formatting preferences belong in the local or global config.
Understand
Try out the Hook Event Simulator: Select a scenario and observe which events fire in what order. Click on individual events to see details and example configurations.
Claude Code writes a file using the Write tool.
Click on a hook event to see details. Use arrow keys to navigate.
Apply
Consider for your current project: Which actions should ALWAYS run after a file change? Which commands should NEVER be executed without a check? The answers will show you which events and handlers you need. In the next section, you'll get ready-made recipes to copy.