Hooks
Hooks are shell commands, LLM prompts, multi-turn agents, or HTTP calls that execute in response to Claude Code events. They can block tool calls, post-process results, transform prompts, auto-format files, and control permissions programmatically.
2 blocks the operation and sends stderr to the model.
Hook Sources (Priority Order)
| # | Source | Location | Scope |
|---|---|---|---|
| 1 | Policy (managed) | Managed settings | Enterprise-wide, highest authority |
| 2 | User settings | ~/.claude/settings.json | Personal, all projects |
| 3 | Project settings | .claude/settings.json | Shared with team |
| 4 | Local settings | .claude/settings.local.json | Private per project |
| 5 | Plugin hooks | ~/.claude/plugins/*/hooks/hooks.json | Plugin-scoped |
| 6 | Session hooks | In-memory (from skills/agents) | Temporary, current session only |
| 7 | Function hooks | In-memory (SDK/plugins) | Programmatic validation |
All matching hooks for a given event run in parallel.
All 27 Hook Events
Tool Lifecycle
| PreToolUse | Before tool runs. Can block or modify input. |
| PostToolUse | After tool succeeds. Can inject context. |
| PostToolUseFailure | After tool fails or is interrupted. |
Session
| SessionStart | startup / resume / clear / compact |
| SessionEnd | clear / resume / logout / exit / other |
| UserPromptSubmit | When user submits a prompt. Can block. |
| Stop | Turn completed (no more tool calls). |
| StopFailure | Turn failed with an error. |
| Setup | One-time setup: init / maintenance. |
Permissions
| PermissionRequest | Before permission dialog shows. Can auto-approve. |
| PermissionDenied | After user denies. Can retry. |
| Notification | When Claude sends a notification. |
Subagents & Teams
| SubagentStart | When a subagent is spawned. |
| SubagentStop | When a subagent completes. |
| TeammateIdle | Teammate has no work. |
| TaskCreated | Task created in a team. |
| TaskCompleted | Task completed. |
Context
| PreCompact | Before compaction. manual / auto. |
| PostCompact | After compaction. Includes summary. |
| InstructionsLoaded | When CLAUDE.md / memory files load. |
| ConfigChange | Settings files changed. |
MCP & Filesystem
| Elicitation | MCP server requests user input. |
| ElicitationResult | After MCP elicitation response. |
| WorktreeCreate | Git worktree created. |
| WorktreeRemove | Git worktree removed. |
| CwdChanged | Working directory changed. |
| FileChanged | A watched file changed. |
The 4 Hook Types
command — most common
{
"type": "command",
"command": "prettier --write "$FILE"",
"if": "Write|Edit",
"shell": "bash",
"timeout": 30,
"statusMessage": "Formatting...",
"once": false,
"async": false,
"asyncRewake": false
} timeout — seconds (default: 600)once — auto-remove after first runasync — run in background without blockingasyncRewake — background + wake model on exit 2prompt — LLM evaluation
{
"type": "prompt",
"prompt": "Review for security issues: $ARGUMENTS",
"if": "Write|Edit",
"timeout": 30,
"model": "claude-sonnet-4-6",
"statusMessage": "Reviewing..."
} $ARGUMENTS — replaced with the full input JSONmodel — defaults to small fast modeltimeout — default: 30 secondsagent — multi-turn verification
{
"type": "agent",
"prompt": "Verify tests still pass: $ARGUMENTS",
"timeout": 60,
"model": "claude-sonnet-4-6"
} model — defaults to Haikutimeout — default: 60 secondshttp — webhook / external API
{
"type": "http",
"url": "https://api.example.com/hooks/claude",
"headers": {
"Authorization": "Bearer $API_TOKEN"
},
"allowedEnvVars": ["API_TOKEN"],
"timeout": 30
} allowedEnvVars — whitelist for $VAR expansion in headersConfiguration Format
settings.json structure
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "prettier --check "$FILE"",
"timeout": 10
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "notify-done.sh"
}
]
}
]
}
} Skill frontmatter hooks
---
hooks:
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: "prettier --write $FILE"
--- Registered as in-memory session hooks. Active only during skill invocation (or the session if once: false).
Agent frontmatter hooks
Same YAML format. Stop hooks are automatically converted to SubagentStop hooks, scoped to the agent's ID, and cleaned up when the agent finishes.
Hook Input (stdin) & Output (stdout)
Base input fields (all events)
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/path/to/project",
"hook_event_name": "PreToolUse",
"permission_mode": "default",
"agent_id": "agent-xyz",
"agent_type": "general-purpose"
} Key per-event additions
tool_name, tool_input, tool_use_id // PostToolUse adds: tool_response // PostToolUseFailure adds: error, is_interrupt
source, agent_type, model
stop_hook_active, last_assistant_message // SubagentStop adds: agent_id, agent_transcript_path
tool_name, tool_input, permission_suggestions
file_path, event // FileChanged old_cwd, new_cwd // CwdChanged
Output: plain text or JSON
If stdout starts with {, it's parsed as JSON. Otherwise shown as plain text in the transcript.
{
"continue": true,
"suppressOutput": false,
"stopReason": "string",
"decision": "approve|block",
"reason": "string",
"systemMessage": "shown to user",
"hookSpecificOutput": { }
} hookSpecificOutput by event
permissionDecision: "allow|deny|ask"
permissionDecisionReason: "string"
updatedInput: { } // modify the tool input
additionalContext: "" // injected as context additionalContext: ""
updatedMCPToolOutput: { } // replace MCP output additionalContext: "" initialUserMessage: "" // auto-inject message watchPaths: ["/path/"] // set up FileChanged watchers
decision: {
behavior: "allow|deny",
updatedInput: { },
updatedPermissions: [
{ tool: "Bash(npm*)", behavior: "allow" }
]
} Exit Codes
| Code | Meaning | Behavior |
|---|---|---|
| 0 | Success | Stdout shown in transcript or parsed as JSON |
| 2 | Blocking error | Blocks the operation. Stderr sent to model as feedback. |
| other | Non-blocking error | Shown to user only, does not block the action |
| Event | Exit code 2 behavior |
|---|---|
| PreToolUse | Blocks the tool call. Stderr shown to model. |
| PostToolUse | Shows stderr to model immediately as feedback. |
| UserPromptSubmit | Blocks processing, clears prompt, shows stderr to user. |
| Stop | Shows stderr to model and continues the conversation. |
| SessionStart | Blocking errors are ignored — session starts anyway. |
Matching & the if Filter
matcher field (3 modes)
| Mode | Example |
|---|---|
| Exact | "Write" |
| Pipe-separated | "Write|Edit" |
| Regex | "^Write.*" or ".*" |
No matcher = fires for ALL instances of that event. Matched against tool_name, source, agent_type, etc. depending on the event.
if condition — secondary filter
Uses permission rule syntax. Evaluated before spawning the hook process. Avoids unnecessary process creation.
// Only git commands
"if": "Bash(git *)"
// Only npm publish
"if": "Bash(npm publish:*)"
// Only TypeScript writes
"if": "Write(*.ts)"
// Only edits in api/ directory
"if": "Edit(src/api/*)"
Permission Integration
PreToolUse permission precedence
Hook says "allow"
+ deny rule exists → DENY (rule wins)
+ ask rule exists → FORCE prompt dialog
+ no rules → ALLOW ✓
Hook says "deny" → DENY immediately
Hook says "ask" → Force permission dialog
Multiple hooks: deny > ask > allow A hook allow does NOT bypass deny/ask rules from settings.json. Rules always win.
PermissionRequest — auto-approve in CI
Fires before the permission dialog would show. Hooks can auto-approve or auto-deny without human interaction.
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedPermissions": [
{ "tool": "Bash(npm test:*)",
"behavior": "allow" }
]
}
}
} Async Hooks
Config-based async
{
"type": "command",
"command": "slow-task.sh",
"async": true
} Hook runs in background. Claude continues without waiting.
Runtime async declaration
Hook declares itself async at runtime via its first stdout line:
{"async": true, "asyncTimeout": 30} asyncRewake
{
"type": "command",
"command": "check-ci.sh",
"async": true,
"asyncRewake": true
} When this async hook exits with code 2, it wakes the model via enqueuePendingNotification(). Enables background monitoring patterns.
Environment Variables & Timeouts
Available to command hooks
| CLAUDE_PROJECT_DIR | Root project directory (stable, not worktree path) |
| CLAUDE_PLUGIN_ROOT | Plugin/skill root directory |
| CLAUDE_PLUGIN_DATA | Plugin data directory |
| CLAUDE_PLUGIN_OPTION_* | Plugin option values (UPPERCASE) |
| CLAUDE_ENV_FILE | Path to .sh file for BashTool env vars. SessionStart, Setup, CwdChanged, FileChanged only. |
| CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS | Override SessionEnd timeout (default: 1500ms) |
Default timeouts
| General (command / http) | 10 min |
| SessionEnd hooks | 1.5 sec |
| Prompt hooks | 30 sec |
| Agent hooks | 60 sec |
| Async hooks (asyncTimeout) | 15 sec |
| Function hooks | 5 sec |
The timeout field in hook config is in seconds.
Security policies
allowManagedHooksOnly — only policy hooks executedisableAllHooks — in policySettings: disables all hooks; in other sources: only non-managedInteractive Prompt Elicitation
Command hooks can request user input via a stdout/stdin protocol — enabling interactive multi-step hooks without a separate UI.
Hook writes to stdout:
{
"prompt": "request-id-123",
"message": "Choose deployment target:",
"options": [
{ "key": "staging", "label": "Staging" },
{ "key": "production", "label": "Production" }
]
} Claude Code writes back to hook's stdin:
{
"prompt_response": "request-id-123",
"selected": "staging"
} Practical Examples
Auto-format after file edits
// PostToolUse, matcher: "Write|Edit"
{
"type": "command",
"command": "prettier --write "$(cat | jq -r '.tool_input.file_path')"",
"timeout": 10,
"statusMessage": "Formatting..."
} Block dangerous git commands
// PreToolUse, matcher: "Bash", if: "Bash(git *)"
{
"type": "command",
"command": "CMD=$(cat|jq -r '.tool_input.command'); if echo "$CMD" | grep -qE 'force|reset --hard'; then echo 'Blocked' >&2; exit 2; fi"
} Auto-approve permissions in CI
// PermissionRequest, matcher: "Bash"
{
"type": "command",
"command": "echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'"
} Background CI monitoring (asyncRewake)
// Stop event
{
"type": "command",
"command": "check-ci-status.sh",
"async": true,
"asyncRewake": true
}
// When CI fails, script exits 2 → wakes model .claude/settings.json could execute arbitrary code.
Always verify project-level hooks before accepting workspace trust, and use
allowManagedHooksOnly in enterprise environments to restrict
hooks to policy-controlled sources only.