Claude Code hooks: every event and when it fires
The full list of Claude Code hook events grouped by what they react to, the settings.json shape, and the exit codes that decide whether a tool call runs.
An explanation. Nothing was measured for this one.

- Time
- 9 min
- Effort
- moderate
- Needs
- Claude Code · a JSON editor
A hook is a command Claude Code runs at a defined moment — before a tool call, after a file changes, when a session starts. Most write-ups cover two or three events and leave you guessing whether the one you want exists.
This is the full list, grouped by what each event reacts to, plus the configuration shape and the exit codes that decide whether a tool call actually runs.
The short version
Hooks are configured under hooks in settings.json, keyed by event name. Each entry
has an optional matcher and a list of hooks to run. A PreToolUse hook can block
the call it is inspecting: exit code 2 blocks, exit code 0 allows the normal
permission flow to continue, and any other non-zero code is a non-blocking error.
This page is documentation, not a benchmark. Nothing here was measured.
Every hook event
Tool execution
The events most hooks use. PreToolUse is the only one that can prevent a call.
| Event | Fires |
|---|---|
PreToolUse | Before a tool call runs — can block it |
PostToolUse | After a tool call completes |
PostToolUseFailure | After a tool call fails |
PermissionRequest | When a permission decision is requested |
PermissionDenied | When a call is denied |
PostToolBatch | After a batch of tool calls |
Session and turn
| Event | Fires |
|---|---|
SessionStart | A session begins |
SessionEnd | A session ends |
UserPromptSubmit | You submit a prompt |
UserPromptExpansion | A prompt is expanded |
Stop | A turn stops |
StopFailure | A turn stops because of a failure |
Files and configuration
Useful for reacting to state rather than to the model.
| Event | Fires |
|---|---|
FileChanged | A file changes |
CwdChanged | The working directory changes |
DirectoryAdded | A directory is added |
WorktreeCreate / WorktreeRemove | A git worktree is created or removed |
ConfigChange | Configuration changes |
InstructionsLoaded | Instructions are loaded |
Subagents and tasks
| Event | Fires |
|---|---|
SubagentStart / SubagentStop | A subagent starts or stops |
TaskCreated / TaskCompleted | A task is created or completed |
TeammateIdle | A teammate goes idle |
Model, context and interaction
| Event | Fires |
|---|---|
PreModelSwitch / PostModelSwitch | Around a model switch |
PreCompact / PostCompact | Around context compaction |
Notification | A notification is raised |
MessageDisplay | A message is displayed |
Elicitation / ElicitationResult | An elicitation is requested or answered |
PreCompact is the one people look for and rarely find: it is where you capture
state before context is compacted away.
The configuration shape
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"timeout": 30
}
]
}
]
}
}
The nesting trips people up: the event maps to an array of matcher groups, and
each group has its own hooks array. Two levels, not one.
The fields
| Field | Required | What it does |
|---|---|---|
type | yes | command, http, mcp_tool, prompt or agent |
matcher | no | Filters which calls the hook sees — "Bash", "Edit|Write", "mcp__.*" |
if | no | Narrower filter using permission-rule syntax — "Bash(git *)", "Edit(*.ts)" |
command | for command | Shell command or executable path |
args | no | Argument list; switches to exec form when present |
timeout | no | Seconds before cancelling |
url | for http | Endpoint to POST to |
server / tool | for mcp_tool | MCP server name and tool name |
prompt | for prompt | Prompt text sent to the model |
matcher and if do different jobs and are easy to confuse. matcher filters by
tool name; if filters on the arguments using the same syntax as permission rules.
A hook that should only see TypeScript edits wants matcher: "Edit" with
if: "Edit(*.ts)".
Blocking a tool call
Only PreToolUse can stop a call. There are two ways.
Exit code 2
The simplest, and the one to reach for first. Exit code 2 always blocks, whatever the hook printed on stdout, and stderr is returned to the model as the reason.
#!/usr/bin/env bash
command=$(jq -r '.tool_input.command // empty')
if printf '%s' "$command" | grep -q 'rm -rf'; then
echo "Destructive command blocked by hook" >&2
exit 2
fi
exit 0
JSON with permissionDecision
Use this when you want a structured decision rather than a stderr line:
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive command blocked by hook"
}
}'
permissionDecision accepts "allow" or "deny". Omit the field and the normal
permission flow applies.
The exit codes, together
| Exit code | Effect |
|---|---|
0 | No decision — normal permission flow continues |
2 | Blocks the call, stderr returned as the reason |
| anything else | Non-blocking error; the action proceeds |
That last row is the one that bites. A hook with a typo exits 1, prints an error
nobody reads, and the tool call goes ahead. If your guard must hold, make its
failure path exit 2, not 1.
Reading the payload
The call arrives as JSON on stdin. Which fields matter depends on the tool:
Bash— the command is attool_input.commandEditandWrite— the target path is attool_input.file_path
Two habits worth adopting. Use // empty in your jq expressions so a missing field
gives you an empty string rather than the literal null. And exit 0 when the field
you need is absent, so an unexpected payload shape does not silently block work you
meant to allow.
FAQ
Which events can block a tool call?
Only PreToolUse. The others observe and react; they cannot prevent the action they
are reporting on.
What is the difference between matcher and if?
matcher filters by tool name. if filters on the call’s arguments using permission-rule
syntax. Use matcher to choose the tool and if to narrow within it.
Why did my hook not block anything?
The most common cause is the exit code: only 2 blocks. A hook that exits 1 on
error is reported as a non-blocking error and the call proceeds.
Is there a hook for context compaction?
Yes — PreCompact and PostCompact.
Were these events tested here?
No. This is a reference page compiled from Anthropic’s hooks documentation, with
nothing measured. Verify against the docs for the version you are running, since the
event list grows.
Where this sits
A reference page: documented behaviour, nothing run. For a task that uses one of these events in anger, see stopping out-of-scope edits. What Benchivo has actually measured is in the tests, under the rules in the methodology.