Hooks System
- Entity ID:
ent-20260409-49ff1cea1c44 - Type:
service - Scope:
shared - Status:
active - Aliases: hooks, lifecycle hooks, PreToolUse, PostToolUse
Description
The hooks system exposes 27 lifecycle events that allow users and extensions to intercept, modify, or log Claude Code's behavior at well-defined boundaries. Hooks are configured in settings.json (at project, user, or enterprise scope) and can execute as shell commands, HTTP requests, LLM-based verifiers, or full agent workflows. Each hook receives structured JSON describing the event context and can approve, block, or modify the operation.
Primary implementation: src/utils/hooks.ts (3789 lines)
Lifecycle events (27 total)
Tool & permissions:
| Event | Description |
|-------|-------------|
| PreToolUse | Before tool execution — can block or modify the call |
| PostToolUse | After successful tool execution |
| PostToolUseFailure | When a tool fails |
| PermissionRequest | When permission dialog shown — can auto-allow/deny |
| PermissionDenied | When auto-mode classifier denies |
User input & session:
| Event | Description |
|-------|-------------|
| UserPromptSubmit | When user submits a prompt — can block or modify |
| SessionStart | New session started (startup/resume/clear/compact) |
| SessionEnd | Session ending (tight 1.5s timeout) |
| Setup | Repo setup (init/maintenance triggers) |
Stop & completion:
| Event | Description |
|-------|-------------|
| Stop | Right before Claude concludes response |
| StopFailure | When API error ends the turn (fire-and-forget) |
Subagents & tasks:
| Event | Description |
|-------|-------------|
| SubagentStart | Agent tool call starts |
| SubagentStop | Right before subagent concludes |
| TeammateIdle | Teammate about to go idle |
| TaskCreated | Task being created |
| TaskCompleted | Task being marked complete |
Compaction & MCP:
| Event | Description |
|-------|-------------|
| PreCompact | Before conversation compaction — can block |
| PostCompact | After conversation compaction |
| Elicitation | MCP server requests user input |
| ElicitationResult | After user responds to MCP elicitation |
Configuration & files:
| Event | Description |
|-------|-------------|
| ConfigChange | Config files change during session |
| InstructionsLoaded | CLAUDE.md or rule files loaded (observability-only) |
| CwdChanged | Working directory changes |
| FileChanged | Watched files change (requires CLAUDE_ENV_FILE) |
| Notification | Notifications sent |
Worktree management:
| Event | Description |
|-------|-------------|
| WorktreeCreate | Create isolated worktree |
| WorktreeRemove | Remove worktree |
Hook types
Four execution types, configured via the type field:
Command hooks (default)
Shell commands spawned as child processes. On Windows, uses Git Bash explicitly (not cmd.exe) with POSIX paths.
{
"type": "command",
"command": "my-linter --check",
"shell": "bash",
"timeout": 30,
"async": false
}
PowerShell alternative available via "shell": "powershell" — uses -NoProfile -NonInteractive.
HTTP hooks
POST event JSON to a URL. SSRF-guarded: validates resolved IPs, blocks private ranges. Supports env var interpolation in headers (only vars listed in allowedEnvVars). Policy allowlist via allowedHttpHookUrls.
Prompt hooks
LLM-based verification using Haiku (or custom model). Returns { ok: true } or { ok: false, reason: "..." }. Blocks if not ok. Default timeout: 30s.
Agent hooks
Full agentic verification — multi-turn agent with tool access. Can read transcript via transcript_path. Must use SyntheticOutputTool to return { ok: true|false, reason? }. Default timeout: 60s.
JSON protocol
Stdin — hook receives structured JSON:
{
"session_id": "abc...",
"transcript_path": "/path/to/transcript",
"cwd": "/current/working/directory",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "git status" },
"tool_use_id": "uuid"
}
Event-specific fields vary: PostToolUse adds tool_response, SessionStart adds source (startup/resume/clear/compact), etc.
Stdout — hook may emit JSON response:
{
"continue": true,
"decision": "approve",
"reason": "Command is safe",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": { "command": "git status --short" },
"additionalContext": "Note: limited to short format"
}
}
Exit codes: | Code | Behavior | |------|----------| | 0 | Success. JSON parsed if present. stdout/stderr not shown to user. | | 2 | Blocking error. stderr shown to the model. Operation prevented. | | Other | Non-blocking error. stderr shown to user only. Operation continues. |
PreToolUse blocking and modification
PreToolUse hooks have three control mechanisms:
- Exit code 2 — blocks the tool call. stderr is sent to the model so it can retry or handle the error.
permissionDecision— override the permission system with"allow","deny", or"ask".updatedInput— modify the tool's input arguments before execution. The modified input replaces the original.
These take precedence over the built-in permission system.
Async hooks
A hook can declare itself async by emitting {"async": true} as its first line of stdout. The hook is then backgrounded to the AsyncHookRegistry (src/utils/hooks/AsyncHookRegistry.ts) and the operation proceeds immediately.
Default async timeout: 15s (configurable per hook via asyncTimeout field).
Async rewake (asyncRewake: true): a special mode where the hook runs in the background indefinitely. On completion with exit code 2, it enqueues a task-notification that wakes the model — used for long-running verification that shouldn't block the conversation.
Configuration
Priority order (highest to lowest):
1. User settings (~/.claude/settings.json)
2. Project settings (.claude/settings.json)
3. Local settings (.claude/settings.local.json)
4. Managed/policy settings (~/.claude/settings/policy.json)
5. Plugin hooks
6. Built-in hooks (internal callbacks)
Schema (src/schemas/hooks.ts:176-222):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(git *)", // Permission rule syntax
"hooks": [
{ "type": "command", "command": "verify-git-cmd", "timeout": 10 }
]
}
]
}
}
Matchers support: exact match ("Write"), pipe-separated ("Write|Edit"), regex (^Write.*), and permission rule syntax in if field (Bash(git *)).
Policy controls:
- allowManagedHooksOnly: true — blocks user/project/local hooks; only admin hooks run
- Managed hooks run in all modes (interactive, SDK, etc.)
Timeouts
| Hook category | Default timeout |
|---|---|
| Tool hooks (PreToolUse, PostToolUse, etc.) | 10 minutes (600s) |
| SessionEnd hooks | 1.5 seconds |
| Async hooks | 15 seconds |
| Prompt hooks | 30 seconds |
| Agent hooks | 60 seconds |
| Individual override | timeout field in config (seconds) |
Timeout fires an AbortSignal that kills the child process. The operation proceeds as if the hook wasn't present (fail-open).
Session hooks and function hooks
In addition to config-based hooks, the system supports ephemeral hooks:
- Session hooks (
src/utils/hooks/sessionHooks.ts) — added via SDK withaddSessionHook(). Cleared when session ends. - Function hooks — in-process TypeScript callbacks registered by plugins or SDK. Returns
true(pass) orfalse(block). Skip JSON serialization for performance.
Environment variable injection
Hooks for SessionStart, Setup, CwdChanged, and FileChanged receive CLAUDE_ENV_FILE. Bash hooks can write shell exports (export VAR=value), which accumulate into a session environment script injected into subsequent Bash tool commands. PowerShell hooks skip this (syntax incompatible).
Trust and security
- All hooks require workspace trust —
shouldSkipHookDueToTrust()checks on every execution - Sessions without trust approval skip even SessionEnd/SubagentStop hooks
- HTTP hooks are SSRF-guarded with policy allowlist enforcement and env var sanitization
- Managed hooks bypass user trust requirements
What depends on it
- Tool system — PreToolUse/PostToolUse hooks fire around every tool invocation
- Permission system — hooks can override permission decisions
- Compaction — PreCompact hooks can block compaction
- Agent lifecycle — SubagentStart/SubagentStop hooks track agent execution
- Plugin system — plugins register hooks programmatically
- Bridge system — forwards hook events to IDE extensions
Design trade-offs
| Decision | Trade-off |
|---|---|
| Fail-open on timeout/error | Operations never permanently blocked by broken hooks, but misbehaving hooks silently fail |
| Exit code 2 for blocking | Simple protocol, but only one blocking code — no way to distinguish "denied" from "error" |
| All matching hooks run in parallel | Fast, but hooks can't depend on each other's output |
| JSON on stdin/stdout | Language-agnostic, but requires JSON parsing and can't handle binary data |
| 10-minute default timeout for tool hooks | Generous for complex checks, but a hung hook blocks the tool for 10 minutes |
| Async rewake via task-notification | Non-blocking long verification, but complex implementation with multiple code paths |
Key claims
- 27 lifecycle events spanning session, tool, permission, compaction, agent, and file boundaries
- Four hook types: command (shell), HTTP, prompt (LLM), and agent (multi-turn)
- PreToolUse can block, modify input, and override permissions
- Fail-open by default — hook failures don't block operations
- Policy controls enable enterprise lockdown (managed hooks only)
Relations
rel-hooks-tools: hooks-system --[wraps]--> tool-system (PreToolUse/PostToolUse)rel-hooks-permissions: hooks-system --[overrides]--> permission-pipelinerel-hooks-plugins: plugin-system --[registers]--> hooks-systemrel-hooks-bridge: hooks-system --[forwards-to]--> bridge-system (IDE extensions)
Sources
src-20260409-a5fc157bc756, source code at src/utils/hooks.ts, src/utils/hooks/, src/schemas/hooks.ts