Hooks are shell commands (or HTTP endpoints, or prompts) that the harness runs at named points in Claude’s execution. The most important thing to know before picking an event: exit code 2 blocks the action on blocking events, but has no effect on non-blocking events where the action already completed.
The four-question decision tree
-
Do you need to prevent something from happening? Use a blocking event:
PreToolUse,UserPromptSubmit,Stop,PreCompact,PermissionRequest,PostToolBatch,TaskCreated,TaskCompleted,SubagentStop,TeammateIdle,ConfigChange,WorktreeCreate,Elicitation,ElicitationResult. -
Do you need to react after something happened? Use a non-blocking event:
PostToolUse,PostToolUseFailure,SessionStart,SessionEnd,Setup,Notification,SubagentStart,CwdChanged,FileChanged,PostCompact,InstructionsLoaded,PermissionDenied,WorktreeRemove,StopFailure. -
Is your hook specific to one tool or a group of tools? Set a
matcheronPreToolUseorPostToolUse. Most other events have no matcher support. -
Does your hook need to run once (session init) or on every tool call?
SessionStartfires once per session.PreToolUsefires before every tool call that matches your matcher.
All 28 events
Lifecycle
| Event | Fires when | Blocking? | Common use |
|---|---|---|---|
SessionStart | New session, resume, /clear, compact | No | Load dev context, print git status |
Setup | --init-only, --init, --maintenance | No | One-time dependency install, repo setup |
SessionEnd | Session terminates (clear/resume/logout) | No | Cleanup, final logging |
ConfigChange | A settings file changes on disk | Yes | Block unauthorized policy changes |
CwdChanged | Directory changes via cd command | No | direnv allow, activate virtualenv |
FileChanged | A watched file changes | No | Reload on .env change |
InstructionsLoaded | CLAUDE.md or rules file loaded | No | Audit instruction access |
Tool execution
| Event | Fires when | Blocking? | Common use |
|---|---|---|---|
PreToolUse | Before any tool executes | Yes | Block rm -rf, enforce path rules |
PostToolUse | Tool succeeds | No | Lint after Edit/Write, validate output |
PostToolUseFailure | Tool fails | No | Log failures, notify on error |
PostToolBatch | Parallel batch of tool calls resolves | Yes | Validate all batch results before continuing |
PermissionRequest | Permission dialog appears | Yes | Auto-approve safe read-only commands |
PermissionDenied | Tool denied by auto classifier | No | Allow Claude to retry (set retry: true in JSON output) |
Turn control
| Event | Fires when | Blocking? | Common use |
|---|---|---|---|
UserPromptSubmit | User submits a prompt | Yes | Add git context, filter harmful prompts |
UserPromptExpansion | Slash command expands | Yes | Gate skill access by user role |
Stop | Claude finishes responding | Yes | Gate on tests passing before Claude declares done |
StopFailure | Turn ends due to API error | No | Log API errors, send alert |
Notification | Claude sends a notification | No | Log notifications to external system |
Agent and task events
| Event | Fires when | Blocking? | Common use |
|---|---|---|---|
SubagentStart | A subagent is spawned | No | Monitor agent creation |
SubagentStop | A subagent finishes | Yes | Prevent subagent stop to extend its work |
TeammateIdle | An agent-teams teammate goes idle | Yes | Keep teammate working, assign next task |
TaskCreated | Task created via TaskCreate | Yes | Validate task description before creation |
TaskCompleted | Task marked complete | Yes | Enforce pre-completion checks |
Context management
| Event | Fires when | Blocking? | Common use |
|---|---|---|---|
PreCompact | Before context compaction | Yes | Block premature compaction |
PostCompact | After compaction completes | No | Log tokens removed |
MCP elicitation
| Event | Fires when | Blocking? | Common use |
|---|---|---|---|
Elicitation | MCP server requests user input | Yes | Auto-respond or gate MCP user input |
ElicitationResult | User responds to an elicitation | Yes | Validate or transform user response |
Worktrees
| Event | Fires when | Blocking? | Common use |
|---|---|---|---|
WorktreeCreate | A worktree is created | Yes | Custom worktree setup scripts |
WorktreeRemove | A worktree is removed | No | Cleanup worktree state |
Exit code semantics
Exit 0: allow action, or success (non-blocking)
Exit 1: treated as allow (non-blocking error)
Exit 2: BLOCK action (blocking events) OR show stderr only (non-blocking)
Other: allow / continue with non-blocking error
Stop uses a different protocol than most blocking events. Return JSON on stdout:
{
"decision": "block",
"reason": "Tests are failing. Fix before declaring done."
}
Without the JSON protocol, the Stop hook output is treated as a stop-hook-error subtype and Claude reads it as an error to diagnose.
Key payload fields
PreToolUse / PostToolUse:
{
"tool_name": "Bash",
"tool_input": {"command": "npm test"},
"tool_use_id": "abc123",
"tool_response": "..."
}
UserPromptSubmit:
{
"prompt": "the user's prompt text"
}
SessionStart:
{
"source": "startup|resume|clear|compact",
"model": "claude-sonnet-4-6"
}
ConfigChange:
{
"config_source": "user_settings|project_settings|local_settings|policy|skills"
}
Footguns
PostToolUse exit code 2 does not undo the tool. The edit or bash command already ran. Exit 2 on PostToolUse shows stderr to Claude as feedback but cannot reverse what happened. If you need to prevent something, use PreToolUse.
UserPromptSubmit has no matcher support. It fires on every prompt regardless of content. A hook that runs a slow script on every prompt adds that latency to every turn. Keep UserPromptSubmit hooks fast, or use PreToolUse with a specific matcher if you only care about tool invocations.
Stop hook gaming. A Stop hook that exits 2 when tests fail causes Claude to keep working. But Claude can resolve the failing test by editing the test assertion itself, which technically makes the tests pass. Gating on tests gets compliance, not correctness. Combine with a PreToolUse hook that denies edits to test files if you want to prevent this.
PostToolBatch blocks the entire agentic loop, not just one call. Exit code 2 on PostToolBatch stops forward progress for the entire parallel batch. Use it for cases where a bad result in any one call should halt everything, not for per-call validation (use PostToolUse for that).
Matcher mode mismatch between list and regex. If your PreToolUse matcher is a comma-separated list ("Edit,Write"), it matches exact tool names. If it is a regex, it is a pattern match. Mixing these by accident is a common source of hooks that fire unexpectedly or not at all. Verify matcher type by checking whether your matcher string contains regex metacharacters.
When NOT to write a hook
- For something
permissions.denycan already express.PreToolUseto blockrm -rfis redundant if you haveBash(rm -rf *)in deny rules. Deny rules run before hooks. - For formatting on every file write. A slow formatter on every
PostToolUseadds latency to every multi-file refactor. Run formatting once atStopinstead. - For instrumentation in read-only sessions. Hooks add noise to exploratory or audit-only sessions where Claude makes no changes. Gate hooks on tool type.