The Hooks System

22 hook events, 4 handler types, and full configuration reference

Hooks are lifecycle callbacks that fire at specific points during Claude’s execution — before a tool runs, after a file is edited, when a session starts, and 19 other moments. With 22 event types and 4 handler types (command, HTTP, prompt, agent), hooks are the deepest extensibility point in the CLI. This chapter is the full configuration reference; for security-focused patterns like blocking destructive commands, see the Hooks as Guardrails chapter.

Hook event flow: event, matcher, handler, exit code with allow and block branchesEventMatcherHandlerExit CodeAllow (exit 0)Block (exit 1)

Event Types

Every hook targets one of 22 events. Each event fires at a specific moment in Claude’s lifecycle and optionally supports a matcher to narrow scope.

Session & Configuration Events

EventWhen It FiresMatcher
SessionStartSession begins or resumesSession source: startup, resume, clear, compact
SessionEndSession terminatesExit reason: clear, logout, prompt_input_exit
ConfigChangeA config file changesConfig source: user_settings, project_settings, etc.
InstructionsLoadedCLAUDE.md or rules loadedNo matcher support

User Interaction Events

EventWhen It FiresMatcher
UserPromptSubmitUser submits a promptNo matcher support
PermissionRequestPermission dialog appearsTool name
NotificationClaude sends a notificationNotification type: permission_prompt, idle_prompt

Tool Execution Events

EventWhen It FiresMatcher
PreToolUseBefore a tool executesTool name: Bash, Edit|Write, mcp__.*
PostToolUseAfter a tool succeedsTool name
PostToolUseFailureAfter a tool failsTool name

Agent Management Events

EventWhen It FiresMatcher
SubagentStartSubagent spawnedAgent type name
SubagentStopSubagent finishesAgent type name
StopMain Claude finishes respondingNo matcher support
TeammateIdleTeam agent going idleNone
TaskCompletedTask marked completeNone

Context & Isolation Events

EventWhen It FiresMatcher
PreCompactBefore context compactionTrigger: manual, auto
PostCompactAfter context compactionTrigger: manual, auto
WorktreeCreateWorktree being createdNo matcher support
WorktreeRemoveWorktree being removedNo matcher support

MCP Integration Events

EventWhen It FiresMatcher
ElicitationMCP server requests user inputNone
ElicitationResultUser responds to MCP elicitationNone

Handler Types

Each hook entry specifies one of four handler types that determine how the hook runs.

Handler Types

TypeBehaviorUse Case
commandExecutes a shell command. Hook input arrives on stdin as JSON. Supports timeout, async, and statusMessage options.Bash validators, linters, log scripts, any local automation
httpSends a POST request with JSON body to a URL. Supports headers with env var interpolation and allowedEnvVars whitelisting.Audit logging to external services, webhook-based policy enforcement
promptSends hook input to a Claude model for evaluation. Uses $ARGUMENTS placeholder in the prompt text, replaced with the JSON input.AI-powered command validation, semantic policy checks
agentSpawns a subagent with tool access to evaluate the hook input. Most powerful but slowest handler type.Complex verification tasks that require reading files or running commands

Command Handler

The most common handler type. The shell command receives hook context on stdin as JSON and communicates its decision via exit codes.

{
"type": "command",
"command": "./.claude/hooks/validator.sh",
"timeout": 600,
"async": false,
"statusMessage": "Validating..."
}
  • command (required): shell command to run
  • timeout (optional): seconds before cancel, default 600
  • async (optional): run in background without blocking Claude
  • statusMessage (optional): spinner text shown during execution

HTTP Handler

Posts the JSON input to an external endpoint. Headers support environment variable interpolation with $VAR_NAME syntax, but only for variables listed in allowedEnvVars.

{
"type": "http",
"url": "http://localhost:8080/hooks/validate",
"headers": {"Authorization": "Bearer $MY_TOKEN"},
"allowedEnvVars": ["MY_TOKEN"],
"timeout": 30
}

Prompt Handler

Delegates the decision to a Claude model. The $ARGUMENTS placeholder in your prompt text is replaced with the full JSON input from the hook event.

{
"type": "prompt",
"prompt": "Should this bash command run? $ARGUMENTS\n\nDeny commands with 'rm -rf'.",
"model": "claude-3-5-haiku-20241022",
"timeout": 30
}

Agent Handler

Spawns a full subagent with tool access. Use this when evaluation requires reading files, running commands, or multi-step reasoning.

{
"type": "agent",
"prompt": "Verify this operation is safe: $ARGUMENTS",
"timeout": 60
}

Configuration

Hooks are defined in settings.json under the hooks key. The structure nests event name, then an array of matcher+hooks pairs, then an array of handler objects within each pair.

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/block-rm.sh"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "./scripts/validate-edit.sh",
"timeout": 10,
"statusMessage": "Validating edit..."
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "./scripts/run-linter.sh",
"timeout": 30,
"statusMessage": "Running linter..."
}
]
}
]
}
}

Configuration Scopes

Hooks can be defined at multiple levels, from personal defaults to project-specific rules.

Where to Put Hooks

LocationScopeUse Case
~/.claude/settings.jsonAll projectsPersonal safety rules that follow you everywhere
.claude/settings.jsonThis projectTeam-shared hooks, committed to git
.claude/settings.local.jsonThis project, local onlyLocal-only hooks, gitignored
Plugin hooks/hooks.jsonWhere plugin is enabledDistributed hooks (only http and prompt types allowed)
Agent/Skill frontmatterWhile component is activeHooks scoped to a specific agent or skill lifecycle
Gotcha

Hooks do not walk up the directory tree. A parent directory’s .claude/settings.json hooks do not fire when running from a subfolder — even if the subfolder has no settings.json of its own. This is different from skills and MCP configs (which do inherit upward). Each subfolder that needs hook enforcement must define its own .claude/settings.json. For org-wide enforcement, use managed settings.

The /hooks Browser

Inside an interactive Claude session, type /hooks to open the built-in hook browser. It lists all registered hooks across every scope, shows which events they target, and lets you verify your configuration is loaded correctly. Pair it with --debug "hooks" when troubleshooting execution order or matcher issues.

Event Matchers

Matchers are regex patterns that filter which specific instances of an event trigger your hook. Without a matcher, the hook fires for every occurrence of that event type.

{"matcher": "Bash"} // Exact tool name
{"matcher": "Edit|Write"} // Multiple tools (regex OR)
{"matcher": "mcp__memory__.*"} // All MCP memory server tools
{"matcher": "mcp__.*__write.*"} // Any MCP write operation

Matchers are case-sensitive and use full regex syntax. This means Bash* does not mean “anything starting with Bash” — it means “Bas” followed by zero or more “h” characters. For exact tool matching, just use the tool name directly: Bash.

For Notification events, the matcher targets the notification type (permission_prompt, idle_prompt) rather than a tool name. For SessionStart, it matches the session source (startup, resume, clear, compact).

No Matcher = Fires for Everything

Omitting the matcher field means the hook runs on every event of that type. For PreToolUse, that means every single tool call — Bash, Edit, Write, Read, Glob, Grep, and all MCP tools. Be intentional about whether you want broad or narrow scope.

Exit Code Behavior

Command handlers communicate their decision through exit codes. The convention is specific to the hooks system and differs from typical Unix expectations.

Exit Codes

Exit CodeMeaningBehavior
0Success / AllowAction proceeds. If the hook printed JSON to stdout, it is processed for advanced control.
2BlockAction is prevented. Stderr output is fed back to Claude as the reason.
OtherNon-blocking errorHook failure is logged. Stderr shown in verbose mode. Action still proceeds.

What exit code 2 blocks depends on the event:

Exit Code 2 Blocking by Event

EventWhat Gets Blocked
PreToolUseTool call is prevented entirely
PermissionRequestPermission is denied
UserPromptSubmitPrompt is not processed
Stop, SubagentStopAgent is prevented from stopping
ConfigChangeConfig change is blocked
PostToolUseNothing — the tool already ran. Stderr is shown but the action cannot be undone.

Hooks in the same event array run sequentially in array order. The first hook to exit with code 2 wins — subsequent hooks in the array are not executed.

Exit Codes Are Reversed from Unix Convention

In standard Unix, exit code 0 means success and non-zero means failure. The hooks system reuses 0 for “allow” but specifically reserves exit code 2 for “block.” Exit code 1 (the typical Unix error) is treated as a non-blocking hook failure, not a block. If your script accidentally exits with 1, the action will still proceed — only exit code 2 actually prevents anything.

Hooks Receive Tool Input as Stdin JSON

Command handlers do not receive arguments on the command line. Instead, the full hook context — including tool_input, tool name, and event metadata — arrives as a JSON object on stdin. Your script must read from stdin (e.g., INPUT=$(cat)) and parse it with a tool like jq. Forgetting to read stdin is the most common cause of hooks that silently do nothing.

Environment Variables

Command hooks automatically receive several environment variables:

  • CLAUDE_PROJECT_DIR — project root directory
  • CLAUDE_PLUGIN_ROOT — plugin installation directory
  • CLAUDE_PLUGIN_DATA — plugin persistent data directory
  • CLAUDE_ENV_FILE — (SessionStart only) file path for persisting environment variables across the session

The CLAUDE_ENV_FILE variable deserves special attention. In a SessionStart hook, you can write export statements to this file to set environment variables that persist for all subsequent Bash commands in the session:

.claude/hooks/setup-env.sh
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi
exit 0
Tip

Use —debug “hooks” to see exactly which hooks fire, in what order, and what input they receive. This is the fastest way to diagnose hooks that are not matching or are receiving unexpected data.