AI Assistant

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:

EventFires When
PreToolUseBefore a tool is executed. Can approve, block, or modify input.
PostToolUseAfter a tool executes successfully. Can inject additional context.
PostToolUseFailureAfter a tool execution fails.
PermissionRequestWhen a tool needs user permission. Can auto-approve or deny.
PermissionDeniedAfter 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:

  1. Replace $ARGUMENTS in the prompt with the hook input JSON
  2. Create a user message from the processed prompt
  3. Start a query with a combined abort signal (parent signal + timeout)
  4. The agent can use tools to verify conditions, read files, run commands
  5. 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:

SourceLocation
User settings~/.claude/settings.json
Project settings.claude/settings.json
Local settings.claude/settings.local.json
Policy settingsManaged enterprise settings
Skill frontmatterhooks: field in skill YAML
PluginshooksConfig in plugin definitions
Bundled skillshooks property on BundledSkillDefinition