Hooks System
Hooks are lifecycle callbacks that run at specific points during a Claude Code session. They enable custom validation, automation, and integration without modifying core logic.
Hook Events
The complete list of hook events, defined in HOOK_EVENTS:
| Event | Fires When |
|---|---|
PreToolUse | Before a tool is executed. Can approve, block, or modify input. |
PostToolUse | After a tool executes successfully. Can inject additional context. |
PostToolUseFailure | After a tool execution fails. |
PermissionRequest | When a tool needs user permission. Can auto-approve or deny. |
PermissionDenied | After permission is denied for a tool. |
Hook Types
Hooks come in four execution types, defined in HookCommandSchema:
Shell Command Hooks
Execute a shell command and optionally parse structured JSON output from stdout:
type BashCommandHook = {
type: 'command'
command: string // Shell command to execute
if?: string // Permission rule syntax filter
shell?: 'bash' | 'powershell'
timeout?: number // Seconds
statusMessage?: string // Spinner text
once?: boolean // Run once then remove
async?: boolean // Run in background
asyncRewake?: boolean // Background + wake on exit code 2
}Prompt Hooks
Evaluate a prompt with an LLM:
type PromptHook = {
type: 'prompt'
prompt: string // LLM prompt ($ARGUMENTS for input)
if?: string // Permission rule syntax filter
timeout?: number // Seconds
model?: string // e.g., "claude-sonnet-4-6"
statusMessage?: string
once?: boolean
}Agent Hooks
Run a multi-turn LLM agent with tool access for verification tasks:
type AgentHook = {
type: 'agent'
prompt: string // Verification prompt ($ARGUMENTS for input)
if?: string // Permission rule syntax filter
timeout?: number // Seconds (default 60)
model?: string // e.g., "claude-sonnet-4-6"
statusMessage?: string
once?: boolean
}HTTP Hooks
POST the hook input JSON to a URL:
type HttpHook = {
type: 'http'
url: string // URL to POST to
if?: string // Permission rule syntax filter
timeout?: number // Seconds
headers?: Record<string, string> // Supports $VAR interpolation
allowedEnvVars?: string[] // Env vars allowed in header interpolation
statusMessage?: string
once?: boolean
}HTTP hook header values can reference environment variables using $VAR_NAME syntax, but only variables explicitly listed in allowedEnvVars are interpolated. Unlisted variables resolve to empty strings. This prevents accidental credential leakage.
Hook Configuration Schema
Hooks are configured in settings.json or skill frontmatter using the HooksSchema:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "echo 'About to write a file'",
"if": "Write(*.ts)"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": \"Project uses pnpm\"}}'"
}
]
}
]
}
}Matcher Configuration
Each hook event maps to an array of HookMatcher objects:
type HookMatcher = {
matcher?: string // Pattern to match (e.g., tool name "Write")
hooks: HookCommand[]
}When matcher is omitted, the hooks fire for all occurrences of that event.
The if Condition
The if field uses permission rule syntax to filter hooks before spawning. It evaluates against the hook input's tool_name and tool_input:
{
"type": "command",
"command": "run-linter",
"if": "Bash(npm test *)"
}This avoids spawning hook processes for non-matching tool calls.
Hook Execution
Agent Hook Execution
Agent hooks (execAgentHook) run a multi-turn LLM query with tool access:
- Replace
$ARGUMENTSin the prompt with the hook input JSON - Create a user message from the processed prompt
- Start a query with a combined abort signal (parent signal + timeout)
- The agent can use tools to verify conditions, read files, run commands
- Structured output is enforced via a synthetic output tool
The default timeout is 60 seconds. The agent uses Haiku by default unless a model override is specified.
Sync vs. Async Hook Responses
Hook JSON output follows a discriminated union:
// Synchronous response (blocking)
type SyncHookJSONOutput = {
continue?: boolean // Default: true
suppressOutput?: boolean // Hide stdout from transcript
stopReason?: string // Message when continue is false
decision?: 'approve' | 'block'
reason?: string
systemMessage?: string // Warning shown to user
hookSpecificOutput?: { ... }
}
// Asynchronous response (non-blocking)
type AsyncHookJSONOutput = {
async: true
asyncTimeout?: number
}Hook-Specific Output
The hookSpecificOutput field is event-specific. For example:
- PreToolUse: Can return
permissionDecision,updatedInput,additionalContext - SessionStart: Can return
additionalContext,initialUserMessage,watchPaths - PostToolUse: Can return
additionalContext,updatedMCPToolOutput - PermissionRequest: Can return allow/deny decisions with updated inputs and permissions
Callback Hooks
Internal hooks use the HookCallback type for in-process execution:
type HookCallback = {
type: 'callback'
callback: (
input: HookInput,
toolUseID: string | null,
abort: AbortSignal | undefined,
hookIndex?: number,
context?: HookCallbackContext,
) => Promise<HookJSONOutput>
timeout?: number
internal?: boolean // Excluded from metrics
}HookCallbackContext provides access to getAppState() and updateAttributionState() for hooks that need to read or modify application state.
Hook Result Aggregation
When multiple hooks fire for the same event, results are aggregated into AggregatedHookResult:
type AggregatedHookResult = {
message?: Message
blockingErrors?: HookBlockingError[]
preventContinuation?: boolean
stopReason?: string
permissionBehavior?: PermissionResult['behavior']
additionalContexts?: string[]
updatedInput?: Record<string, unknown>
permissionRequestResult?: PermissionRequestResult
retry?: boolean
}Blocking errors from any hook prevent the operation from proceeding. Permission decisions follow a "most restrictive wins" policy: if any hook blocks, the action is blocked.
Hook Sources
Hooks can be registered from multiple sources, merged at runtime:
| Source | Location |
|---|---|
| User settings | ~/.claude/settings.json |
| Project settings | .claude/settings.json |
| Local settings | .claude/settings.local.json |
| Policy settings | Managed enterprise settings |
| Skill frontmatter | hooks: field in skill YAML |
| Plugins | hooksConfig in plugin definitions |
| Bundled skills | hooks property on BundledSkillDefinition |