DevelopersReference

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.

Benchivo cover for the Claude Code hooks reference: the headline EVERY HOOK EVENT over a teal timeline with marked points where a hook can interrupt it.
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.

EventFires
PreToolUseBefore a tool call runs — can block it
PostToolUseAfter a tool call completes
PostToolUseFailureAfter a tool call fails
PermissionRequestWhen a permission decision is requested
PermissionDeniedWhen a call is denied
PostToolBatchAfter a batch of tool calls

Session and turn

EventFires
SessionStartA session begins
SessionEndA session ends
UserPromptSubmitYou submit a prompt
UserPromptExpansionA prompt is expanded
StopA turn stops
StopFailureA turn stops because of a failure

Files and configuration

Useful for reacting to state rather than to the model.

EventFires
FileChangedA file changes
CwdChangedThe working directory changes
DirectoryAddedA directory is added
WorktreeCreate / WorktreeRemoveA git worktree is created or removed
ConfigChangeConfiguration changes
InstructionsLoadedInstructions are loaded

Subagents and tasks

EventFires
SubagentStart / SubagentStopA subagent starts or stops
TaskCreated / TaskCompletedA task is created or completed
TeammateIdleA teammate goes idle

Model, context and interaction

EventFires
PreModelSwitch / PostModelSwitchAround a model switch
PreCompact / PostCompactAround context compaction
NotificationA notification is raised
MessageDisplayA message is displayed
Elicitation / ElicitationResultAn 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

FieldRequiredWhat it does
typeyescommand, http, mcp_tool, prompt or agent
matchernoFilters which calls the hook sees — "Bash", "Edit|Write", "mcp__.*"
ifnoNarrower filter using permission-rule syntax — "Bash(git *)", "Edit(*.ts)"
commandfor commandShell command or executable path
argsnoArgument list; switches to exec form when present
timeoutnoSeconds before cancelling
urlfor httpEndpoint to POST to
server / toolfor mcp_toolMCP server name and tool name
promptfor promptPrompt 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 codeEffect
0No decision — normal permission flow continues
2Blocks the call, stderr returned as the reason
anything elseNon-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 at tool_input.command
  • Edit and Write — the target path is at tool_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.

← All developers