[proxy] code.claude.com← back | site home | direct (HTTPS) ↗ | proxy home | ◑ dark◐ light

Hooks reference - Claude Code Docs

Hooks are user-defined shell commands, HTTP endpoints, or LLM prompts that execute automatically at specific points in Claude Code’s lifecycle. Use this reference to look up event schemas, configuration options, JSON input/output formats, and advanced features like async hooks, HTTP hooks, and MCP tool hooks. If you’re setting up hooks for the first time, start with the guide instead.

Hook lifecycle

Hooks fire at specific points during a Claude Code session. When an event fires and a matcher matches, Claude Code passes JSON context about the event to your hook handler. For command hooks, input arrives on stdin. For HTTP hooks, it arrives as the POST request body. Your handler can then inspect the input, take action, and optionally return a decision. Some events fire once per session, while others fire repeatedly inside the agentic loop:

The table below summarizes when each event fires. The Hook events section documents the full input schema and decision control options for each one.

EventWhen it fires
SessionStartWhen a session begins or resumes
UserPromptSubmitWhen you submit a prompt, before Claude processes it
PreToolUseBefore a tool call executes. Can block it
PermissionRequestWhen a permission dialog appears
PostToolUseAfter a tool call succeeds
PostToolUseFailureAfter a tool call fails
NotificationWhen Claude Code sends a notification
SubagentStartWhen a subagent is spawned
SubagentStopWhen a subagent finishes
StopWhen Claude finishes responding
TeammateIdleWhen an agent team teammate is about to go idle
TaskCompletedWhen a task is being marked as completed
ConfigChangeWhen a configuration file changes during a session
WorktreeCreateWhen a worktree is being created via --worktree or isolation: "worktree". Replaces default git behavior
WorktreeRemoveWhen a worktree is being removed, either at session exit or when a subagent finishes
PreCompactBefore context compaction
SessionEndWhen a session terminates

How a hook resolves

To see how these pieces fit together, consider this PreToolUse hook that blocks destructive shell commands. The hook runs block-rm.sh before every Bash tool call:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

The script reads the JSON input from stdin, extracts the command, and returns a permissionDecision of "deny" if it contains rm -rf:

#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0  # allow the command
fi

Now suppose Claude Code decides to run Bash "rm -rf /tmp/build". Here’s what happens:

1

2

3

4

The Configuration section below documents the full schema, and each hook event section documents what input your command receives and what output it can return.

Configuration

Hooks are defined in JSON settings files. The configuration has three levels of nesting:

  1. Choose a hook event to respond to, like PreToolUse or Stop
  2. Add a matcher group to filter when it fires, like “only for the Bash tool”
  3. Define one or more hook handlers to run when matched

See How a hook resolves above for a complete walkthrough with an annotated example.

Hook locations

Where you define a hook determines its scope:

LocationScopeShareable
~/.claude/settings.jsonAll your projectsNo, local to your machine
.claude/settings.jsonSingle projectYes, can be committed to the repo
.claude/settings.local.jsonSingle projectNo, gitignored
Managed policy settingsOrganization-wideYes, admin-controlled
Plugin hooks/hooks.jsonWhen plugin is enabledYes, bundled with the plugin
Skill or agent frontmatterWhile the component is activeYes, defined in the component file

For details on settings file resolution, see settings. Enterprise administrators can use allowManagedHooksOnly to block user, project, and plugin hooks. See Hook configuration.

Matcher patterns

The matcher field is a regex string that filters when hooks fire. Use "*", "", or omit matcher entirely to match all occurrences. Each event type matches on a different field:

EventWhat the matcher filtersExample matcher values
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequesttool nameBash, Edit|Write, mcp__.*
SessionStarthow the session startedstartup, resume, clear, compact
SessionEndwhy the session endedclear, logout, prompt_input_exit, bypass_permissions_disabled, other
Notificationnotification typepermission_prompt, idle_prompt, auth_success, elicitation_dialog
SubagentStartagent typeBash, Explore, Plan, or custom agent names
PreCompactwhat triggered compactionmanual, auto
SubagentStopagent typesame values as SubagentStart
ConfigChangeconfiguration sourceuser_settings, project_settings, local_settings, policy_settings, skills
UserPromptSubmit, Stop, TeammateIdle, TaskCompleted, WorktreeCreate, WorktreeRemoveno matcher supportalways fires on every occurrence

The matcher is a regex, so Edit|Write matches either tool and Notebook.* matches any tool starting with Notebook. The matcher runs against a field from the JSON input that Claude Code sends to your hook on stdin. For tool events, that field is tool_name. Each hook event section lists the full set of matcher values and the input schema for that event. This example runs a linting script only when Claude writes or edits a file:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/lint-check.sh"
          }
        ]
      }
    ]
  }
}

UserPromptSubmit, Stop, TeammateIdle, TaskCompleted, WorktreeCreate, and WorktreeRemove don’t support matchers and always fire on every occurrence. If you add a matcher field to these events, it is silently ignored.

Match MCP tools

MCP server tools appear as regular tools in tool events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest), so you can match them the same way you match any other tool name. MCP tools follow the naming pattern mcp__<server>__<tool>, for example:

Use regex patterns to target specific MCP tools or groups of tools:

This example logs all memory server operations and validates write operations from any MCP server:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__memory__.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
          }
        ]
      },
      {
        "matcher": "mcp__.*__write.*",
        "hooks": [
          {
            "type": "command",
            "command": "/home/user/scripts/validate-mcp-write.py"
          }
        ]
      }
    ]
  }
}

Hook handler fields

Each object in the inner hooks array is a hook handler: the shell command, HTTP endpoint, LLM prompt, or agent that runs when the matcher matches. There are four types:

Common fields

These fields apply to all hook types:

FieldRequiredDescription
typeyes"command", "http", "prompt", or "agent"
timeoutnoSeconds before canceling. Defaults: 600 for command, 30 for prompt, 60 for agent
statusMessagenoCustom spinner message displayed while the hook runs
oncenoIf true, runs only once per session then is removed. Skills only, not agents. See Hooks in skills and agents

Command hook fields

In addition to the common fields, command hooks accept these fields:

FieldRequiredDescription
commandyesShell command to execute
asyncnoIf true, runs in the background without blocking. See Run hooks in the background

HTTP hook fields

In addition to the common fields, HTTP hooks accept these fields:

FieldRequiredDescription
urlyesURL to send the POST request to
headersnoAdditional HTTP headers as key-value pairs. Values support environment variable interpolation using $VAR_NAME or ${VAR_NAME} syntax. Only variables listed in allowedEnvVars are resolved
allowedEnvVarsnoList of environment variable names that may be interpolated into header values. References to unlisted variables are replaced with empty strings. Required for any env var interpolation to work

Claude Code sends the hook’s JSON input as the POST request body with Content-Type: application/json. The response body uses the same JSON output format as command hooks. Error handling differs from command hooks: non-2xx responses, connection failures, and timeouts all produce non-blocking errors that allow execution to continue. To block a tool call or deny a permission, return a 2xx response with a JSON body containing decision: "block" or a hookSpecificOutput with permissionDecision: "deny". This example sends PreToolUse events to a local validation service, authenticating with a token from the MY_TOKEN environment variable:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "http",
            "url": "http://localhost:8080/hooks/pre-tool-use",
            "timeout": 30,
            "headers": {
              "Authorization": "Bearer $MY_TOKEN"
            },
            "allowedEnvVars": ["MY_TOKEN"]
          }
        ]
      }
    ]
  }
}

Prompt and agent hook fields

In addition to the common fields, prompt and agent hooks accept these fields:

FieldRequiredDescription
promptyesPrompt text to send to the model. Use $ARGUMENTS as a placeholder for the hook input JSON
modelnoModel to use for evaluation. Defaults to a fast model

All matching hooks run in parallel, and identical handlers are deduplicated automatically. Command hooks are deduplicated by command string, and HTTP hooks are deduplicated by URL. Handlers run in the current directory with Claude Code’s environment. The $CLAUDE_CODE_REMOTE environment variable is set to "true" in remote web environments and not set in the local CLI.

Reference scripts by path

Use environment variables to reference hook scripts relative to the project or plugin root, regardless of the working directory when the hook runs:

This example uses $CLAUDE_PROJECT_DIR to run a style checker from the project’s .claude/hooks/ directory after any Write or Edit tool call:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
          }
        ]
      }
    ]
  }
}

Define plugin hooks in hooks/hooks.json with an optional top-level description field. When a plugin is enabled, its hooks merge with your user and project hooks.This example runs a formatting script bundled with the plugin:

{
  "description": "Automatic code formatting",
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

See the plugin components reference for details on creating plugin hooks.

Hooks in skills and agents

In addition to settings files and plugins, hooks can be defined directly in skills and subagents using frontmatter. These hooks are scoped to the component’s lifecycle and only run when that component is active. All hook events are supported. For subagents, Stop hooks are automatically converted to SubagentStop since that is the event that fires when a subagent completes. Hooks use the same configuration format as settings-based hooks but are scoped to the component’s lifetime and cleaned up when it finishes. This skill defines a PreToolUse hook that runs a security validation script before each Bash command:

---
name: secure-operations
description: Perform operations with security checks
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./scripts/security-check.sh"
---

Agents use the same format in their YAML frontmatter.

Type /hooks in Claude Code to open the interactive hooks manager, where you can view, add, and delete hooks without editing settings files directly. For a step-by-step walkthrough, see Set up your first hook in the guide. Each hook in the menu is labeled with a bracket prefix indicating its source:

Disable or remove hooks

To remove a hook, delete its entry from the settings JSON file, or use the /hooks menu and select the hook to delete it. To temporarily disable all hooks without removing them, set "disableAllHooks": true in your settings file or use the toggle in the /hooks menu. There is no way to disable an individual hook while keeping it in the configuration. The disableAllHooks setting respects the managed settings hierarchy. If an administrator has configured hooks through managed policy settings, disableAllHooks set in user, project, or local settings cannot disable those managed hooks. Only disableAllHooks set at the managed settings level can disable managed hooks. Direct edits to hooks in settings files don’t take effect immediately. Claude Code captures a snapshot of hooks at startup and uses it throughout the session. This prevents malicious or accidental hook modifications from taking effect mid-session without your review. If hooks are modified externally, Claude Code warns you and requires review in the /hooks menu before changes apply.

Hook input and output

Command hooks receive JSON data via stdin and communicate results through exit codes, stdout, and stderr. HTTP hooks receive the same JSON as the POST request body and communicate results through the HTTP response body. This section covers fields and behavior common to all events. Each event’s section under Hook events includes its specific input schema and decision control options.

Common input fields

All hook events receive these fields as JSON, in addition to event-specific fields documented in each hook event section. For command hooks, this JSON arrives via stdin. For HTTP hooks, it arrives as the POST request body.

FieldDescription
session_idCurrent session identifier
transcript_pathPath to conversation JSON
cwdCurrent working directory when the hook is invoked
permission_modeCurrent permission mode: "default", "plan", "acceptEdits", "dontAsk", or "bypassPermissions"
hook_event_nameName of the event that fired

For example, a PreToolUse hook for a Bash command receives this on stdin:

{
  "session_id": "abc123",
  "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
  "cwd": "/home/user/my-project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

The tool_name and tool_input fields are event-specific. Each hook event section documents the additional fields for that event.

Exit code output

The exit code from your hook command tells Claude Code whether the action should proceed, be blocked, or be ignored. Exit 0 means success. Claude Code parses stdout for JSON output fields. JSON output is only processed on exit 0. For most events, stdout is only shown in verbose mode (Ctrl+O). The exceptions are UserPromptSubmit and SessionStart, where stdout is added as context that Claude can see and act on. Exit 2 means a blocking error. Claude Code ignores stdout and any JSON in it. Instead, stderr text is fed back to Claude as an error message. The effect depends on the event: PreToolUse blocks the tool call, UserPromptSubmit rejects the prompt, and so on. See exit code 2 behavior for the full list. Any other exit code is a non-blocking error. stderr is shown in verbose mode (Ctrl+O) and execution continues. For example, a hook command script that blocks dangerous Bash commands:

#!/bin/bash
# Reads JSON input from stdin, checks the command
command=$(jq -r '.tool_input.command' < /dev/stdin)

if [[ "$command" == rm* ]]; then
  echo "Blocked: rm commands are not allowed" >&2
  exit 2  # Blocking error: tool call is prevented
fi

exit 0  # Success: tool call proceeds

Exit code 2 behavior per event

Exit code 2 is the way a hook signals “stop, don’t do this.” The effect depends on the event, because some events represent actions that can be blocked (like a tool call that hasn’t happened yet) and others represent things that already happened or can’t be prevented.

Hook eventCan block?What happens on exit 2
PreToolUseYesBlocks the tool call
PermissionRequestYesDenies the permission
UserPromptSubmitYesBlocks prompt processing and erases the prompt
StopYesPrevents Claude from stopping, continues the conversation
SubagentStopYesPrevents the subagent from stopping
TeammateIdleYesPrevents the teammate from going idle (teammate continues working)
TaskCompletedYesPrevents the task from being marked as completed
ConfigChangeYesBlocks the configuration change from taking effect (except policy_settings)
PostToolUseNoShows stderr to Claude (tool already ran)
PostToolUseFailureNoShows stderr to Claude (tool already failed)
NotificationNoShows stderr to user only
SubagentStartNoShows stderr to user only
SessionStartNoShows stderr to user only
SessionEndNoShows stderr to user only
PreCompactNoShows stderr to user only
WorktreeCreateYesAny non-zero exit code causes worktree creation to fail
WorktreeRemoveNoFailures are logged in debug mode only

HTTP response handling

HTTP hooks use HTTP status codes and response bodies instead of exit codes and stdout:

Unlike command hooks, HTTP hooks cannot signal a blocking error through status codes alone. To block a tool call or deny a permission, return a 2xx response with a JSON body containing the appropriate decision fields.

JSON output

Exit codes let you allow or block, but JSON output gives you finer-grained control. Instead of exiting with code 2 to block, exit 0 and print a JSON object to stdout. Claude Code reads specific fields from that JSON to control behavior, including decision control for blocking, allowing, or escalating to the user.

Your hook’s stdout must contain only the JSON object. If your shell profile prints text on startup, it can interfere with JSON parsing. See JSON validation failed in the troubleshooting guide. The JSON object supports three kinds of fields:

FieldDefaultDescription
continuetrueIf false, Claude stops processing entirely after the hook runs. Takes precedence over any event-specific decision fields
stopReasonnoneMessage shown to the user when continue is false. Not shown to Claude
suppressOutputfalseIf true, hides stdout from verbose mode output
systemMessagenoneWarning message shown to the user

To stop Claude entirely regardless of event type:

{ "continue": false, "stopReason": "Build failed, fix errors before continuing" }

Decision control

Not every event supports blocking or controlling behavior through JSON. The events that do each use a different set of fields to express that decision. Use this table as a quick reference before writing a hook:

EventsDecision patternKey fields
UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop, SubagentStop, ConfigChangeTop-level decisiondecision: "block", reason
TeammateIdle, TaskCompletedExit code onlyExit code 2 blocks the action, stderr is fed back as feedback
PreToolUsehookSpecificOutputpermissionDecision (allow/deny/ask), permissionDecisionReason
PermissionRequesthookSpecificOutputdecision.behavior (allow/deny)
WorktreeCreatestdout pathHook prints absolute path to created worktree. Non-zero exit fails creation
WorktreeRemove, Notification, SessionEnd, PreCompactNoneNo decision control. Used for side effects like logging or cleanup

Here are examples of each pattern in action:

Used by UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop, SubagentStop, and ConfigChange. The only value is "block". To allow the action to proceed, omit decision from your JSON, or exit 0 without any JSON at all:

{
  "decision": "block",
  "reason": "Test suite must pass before proceeding"
}

Uses hookSpecificOutput for richer control: allow, deny, or escalate to the user. You can also modify tool input before it runs or inject additional context for Claude. See PreToolUse decision control for the full set of options.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes are not allowed"
  }
}

Uses hookSpecificOutput to allow or deny a permission request on behalf of the user. When allowing, you can also modify the tool’s input or apply permission rules so the user isn’t prompted again. See PermissionRequest decision control for the full set of options.

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow",
      "updatedInput": {
        "command": "npm run lint"
      }
    }
  }
}

For extended examples including Bash command validation, prompt filtering, and auto-approval scripts, see What you can automate in the guide and the Bash command validator reference implementation.

Hook events

Each event corresponds to a point in Claude Code’s lifecycle where hooks can run. The sections below are ordered to match the lifecycle: from session setup through the agentic loop to session end. Each section describes when the event fires, what matchers it supports, the JSON input it receives, and how to control behavior through output.

SessionStart

Runs when Claude Code starts a new session or resumes an existing session. Useful for loading development context like existing issues or recent changes to your codebase, or setting up environment variables. For static context that does not require a script, use CLAUDE.md instead. SessionStart runs on every session, so keep these hooks fast. The matcher value corresponds to how the session was initiated:

MatcherWhen it fires
startupNew session
resume--resume, --continue, or /resume
clear/clear
compactAuto or manual compaction

SessionStart input

In addition to the common input fields, SessionStart hooks receive source, model, and optionally agent_type. The source field indicates how the session started: "startup" for new sessions, "resume" for resumed sessions, "clear" after /clear, or "compact" after compaction. The model field contains the model identifier. If you start Claude Code with claude --agent <name>, an agent_type field contains the agent name.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "SessionStart",
  "source": "startup",
  "model": "claude-sonnet-4-6"
}

SessionStart decision control

Any text your hook script prints to stdout is added as context for Claude. In addition to the JSON output fields available to all hooks, you can return these event-specific fields:

FieldDescription
additionalContextString added to Claude’s context. Multiple hooks’ values are concatenated
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "My additional context here"
  }
}

Persist environment variables

SessionStart hooks have access to the CLAUDE_ENV_FILE environment variable, which provides a file path where you can persist environment variables for subsequent Bash commands. To set individual environment variables, write export statements to CLAUDE_ENV_FILE. Use append (>>) to preserve variables set by other hooks:

#!/bin/bash

if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
  echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
  echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi

exit 0

To capture all environment changes from setup commands, compare the exported variables before and after:

#!/bin/bash

ENV_BEFORE=$(export -p | sort)

# Run your setup commands that modify the environment
source ~/.nvm/nvm.sh
nvm use 20

if [ -n "$CLAUDE_ENV_FILE" ]; then
  ENV_AFTER=$(export -p | sort)
  comm -13 <(echo "$ENV_BEFORE") <(echo "$ENV_AFTER") >> "$CLAUDE_ENV_FILE"
fi

exit 0

Any variables written to this file will be available in all subsequent Bash commands that Claude Code executes during the session.

UserPromptSubmit

Runs when the user submits a prompt, before Claude processes it. This allows you to add additional context based on the prompt/conversation, validate prompts, or block certain types of prompts.

UserPromptSubmit input

In addition to the common input fields, UserPromptSubmit hooks receive the prompt field containing the text the user submitted.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "Write a function to calculate the factorial of a number"
}

UserPromptSubmit decision control

UserPromptSubmit hooks can control whether a user prompt is processed and add context. All JSON output fields are available. There are two ways to add context to the conversation on exit code 0:

Plain stdout is shown as hook output in the transcript. The additionalContext field is added more discretely. To block a prompt, return a JSON object with decision set to "block":

FieldDescription
decision"block" prevents the prompt from being processed and erases it from context. Omit to allow the prompt to proceed
reasonShown to the user when decision is "block". Not added to context
additionalContextString added to Claude’s context
{
  "decision": "block",
  "reason": "Explanation for decision",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "My additional context here"
  }
}

PreToolUse

Runs after Claude creates tool parameters and before processing the tool call. Matches on tool name: Bash, Edit, Write, Read, Glob, Grep, Agent, WebFetch, WebSearch, and any MCP tool names. Use PreToolUse decision control to allow, deny, or ask for permission to use the tool.

PreToolUse input

In addition to the common input fields, PreToolUse hooks receive tool_name, tool_input, and tool_use_id. The tool_input fields depend on the tool:

Bash

Executes shell commands.

FieldTypeExampleDescription
commandstring"npm test"The shell command to execute
descriptionstring"Run test suite"Optional description of what the command does
timeoutnumber120000Optional timeout in milliseconds
run_in_backgroundbooleanfalseWhether to run the command in background
Write

Creates or overwrites a file.

FieldTypeExampleDescription
file_pathstring"/path/to/file.txt"Absolute path to the file to write
contentstring"file content"Content to write to the file
Edit

Replaces a string in an existing file.

FieldTypeExampleDescription
file_pathstring"/path/to/file.txt"Absolute path to the file to edit
old_stringstring"original text"Text to find and replace
new_stringstring"replacement text"Replacement text
replace_allbooleanfalseWhether to replace all occurrences
Read

Reads file contents.

FieldTypeExampleDescription
file_pathstring"/path/to/file.txt"Absolute path to the file to read
offsetnumber10Optional line number to start reading from
limitnumber50Optional number of lines to read
Glob

Finds files matching a glob pattern.

FieldTypeExampleDescription
patternstring"**/*.ts"Glob pattern to match files against
pathstring"/path/to/dir"Optional directory to search in. Defaults to current working directory
Grep

Searches file contents with regular expressions.

FieldTypeExampleDescription
patternstring"TODO.*fix"Regular expression pattern to search for
pathstring"/path/to/dir"Optional file or directory to search in
globstring"*.ts"Optional glob pattern to filter files
output_modestring"content""content", "files_with_matches", or "count". Defaults to "files_with_matches"
-ibooleantrueCase insensitive search
multilinebooleanfalseEnable multiline matching
WebFetch

Fetches and processes web content.

FieldTypeExampleDescription
urlstring"https://example.com/api"URL to fetch content from
promptstring"Extract the API endpoints"Prompt to run on the fetched content
WebSearch

Searches the web.

FieldTypeExampleDescription
querystring"react hooks best practices"Search query
allowed_domainsarray["docs.example.com"]Optional: only include results from these domains
blocked_domainsarray["spam.example.com"]Optional: exclude results from these domains
Agent

Spawns a subagent.

FieldTypeExampleDescription
promptstring"Find all API endpoints"The task for the agent to perform
descriptionstring"Find API endpoints"Short description of the task
subagent_typestring"Explore"Type of specialized agent to use
modelstring"sonnet"Optional model alias to override the default

PreToolUse decision control

PreToolUse hooks can control whether a tool call proceeds. Unlike other hooks that use a top-level decision field, PreToolUse returns its decision inside a hookSpecificOutput object. This gives it richer control: three outcomes (allow, deny, or ask) plus the ability to modify tool input before execution.

FieldDescription
permissionDecision"allow" bypasses the permission system, "deny" prevents the tool call, "ask" prompts the user to confirm
permissionDecisionReasonFor "allow" and "ask", shown to the user but not Claude. For "deny", shown to Claude
updatedInputModifies the tool’s input parameters before execution. Combine with "allow" to auto-approve, or "ask" to show the modified input to the user
additionalContextString added to Claude’s context before the tool executes
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "My reason here",
    "updatedInput": {
      "field_to_modify": "new value"
    },
    "additionalContext": "Current environment: production. Proceed with caution."
  }
}

PermissionRequest

Runs when the user is shown a permission dialog. Use PermissionRequest decision control to allow or deny on behalf of the user. Matches on tool name, same values as PreToolUse.

PermissionRequest input

PermissionRequest hooks receive tool_name and tool_input fields like PreToolUse hooks, but without tool_use_id. An optional permission_suggestions array contains the “always allow” options the user would normally see in the permission dialog. The difference is when the hook fires: PermissionRequest hooks run when a permission dialog is about to be shown to the user, while PreToolUse hooks run before tool execution regardless of permission status.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PermissionRequest",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf node_modules",
    "description": "Remove node_modules directory"
  },
  "permission_suggestions": [
    { "type": "toolAlwaysAllow", "tool": "Bash" }
  ]
}

PermissionRequest decision control

PermissionRequest hooks can allow or deny permission requests. In addition to the JSON output fields available to all hooks, your hook script can return a decision object with these event-specific fields:

FieldDescription
behavior"allow" grants the permission, "deny" denies it
updatedInputFor "allow" only: modifies the tool’s input parameters before execution
updatedPermissionsFor "allow" only: applies permission rule updates, equivalent to the user selecting an “always allow” option
messageFor "deny" only: tells Claude why the permission was denied
interruptFor "deny" only: if true, stops Claude
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow",
      "updatedInput": {
        "command": "npm run lint"
      }
    }
  }
}

PostToolUse

Runs immediately after a tool completes successfully. Matches on tool name, same values as PreToolUse.

PostToolUse input

PostToolUse hooks fire after a tool has already executed successfully. The input includes both tool_input, the arguments sent to the tool, and tool_response, the result it returned. The exact schema for both depends on the tool.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  },
  "tool_response": {
    "filePath": "/path/to/file.txt",
    "success": true
  },
  "tool_use_id": "toolu_01ABC123..."
}

PostToolUse decision control

PostToolUse hooks can provide feedback to Claude after tool execution. In addition to the JSON output fields available to all hooks, your hook script can return these event-specific fields:

FieldDescription
decision"block" prompts Claude with the reason. Omit to allow the action to proceed
reasonExplanation shown to Claude when decision is "block"
additionalContextAdditional context for Claude to consider
updatedMCPToolOutputFor MCP tools only: replaces the tool’s output with the provided value
{
  "decision": "block",
  "reason": "Explanation for decision",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Additional information for Claude"
  }
}

PostToolUseFailure

Runs when a tool execution fails. This event fires for tool calls that throw errors or return failure results. Use this to log failures, send alerts, or provide corrective feedback to Claude. Matches on tool name, same values as PreToolUse.

PostToolUseFailure input

PostToolUseFailure hooks receive the same tool_name and tool_input fields as PostToolUse, along with error information as top-level fields:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PostToolUseFailure",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite"
  },
  "tool_use_id": "toolu_01ABC123...",
  "error": "Command exited with non-zero status code 1",
  "is_interrupt": false
}
FieldDescription
errorString describing what went wrong
is_interruptOptional boolean indicating whether the failure was caused by user interruption

PostToolUseFailure decision control

PostToolUseFailure hooks can provide context to Claude after a tool failure. In addition to the JSON output fields available to all hooks, your hook script can return these event-specific fields:

FieldDescription
additionalContextAdditional context for Claude to consider alongside the error
{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUseFailure",
    "additionalContext": "Additional information about the failure for Claude"
  }
}

Notification

Runs when Claude Code sends notifications. Matches on notification type: permission_prompt, idle_prompt, auth_success, elicitation_dialog. Omit the matcher to run hooks for all notification types. Use separate matchers to run different handlers depending on the notification type. This configuration triggers a permission-specific alert script when Claude needs permission approval and a different notification when Claude has been idle:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/permission-alert.sh"
          }
        ]
      },
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/idle-notification.sh"
          }
        ]
      }
    ]
  }
}

Notification input

In addition to the common input fields, Notification hooks receive message with the notification text, an optional title, and notification_type indicating which type fired.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "Notification",
  "message": "Claude needs your permission to use Bash",
  "title": "Permission needed",
  "notification_type": "permission_prompt"
}

Notification hooks cannot block or modify notifications. In addition to the JSON output fields available to all hooks, you can return additionalContext to add context to the conversation:

FieldDescription
additionalContextString added to Claude’s context

SubagentStart

Runs when a Claude Code subagent is spawned via the Agent tool. Supports matchers to filter by agent type name (built-in agents like Bash, Explore, Plan, or custom agent names from .claude/agents/).

SubagentStart input

In addition to the common input fields, SubagentStart hooks receive agent_id with the unique identifier for the subagent and agent_type with the agent name (built-in agents like "Bash", "Explore", "Plan", or custom agent names).

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "SubagentStart",
  "agent_id": "agent-abc123",
  "agent_type": "Explore"
}

SubagentStart hooks cannot block subagent creation, but they can inject context into the subagent. In addition to the JSON output fields available to all hooks, you can return:

FieldDescription
additionalContextString added to the subagent’s context
{
  "hookSpecificOutput": {
    "hookEventName": "SubagentStart",
    "additionalContext": "Follow security guidelines for this task"
  }
}

SubagentStop

Runs when a Claude Code subagent has finished responding. Matches on agent type, same values as SubagentStart.

SubagentStop input

In addition to the common input fields, SubagentStop hooks receive stop_hook_active, agent_id, agent_type, agent_transcript_path, and last_assistant_message. The agent_type field is the value used for matcher filtering. The transcript_path is the main session’s transcript, while agent_transcript_path is the subagent’s own transcript stored in a nested subagents/ folder. The last_assistant_message field contains the text content of the subagent’s final response, so hooks can access it without parsing the transcript file.

{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/.../abc123.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "SubagentStop",
  "stop_hook_active": false,
  "agent_id": "def456",
  "agent_type": "Explore",
  "agent_transcript_path": "~/.claude/projects/.../abc123/subagents/agent-def456.jsonl",
  "last_assistant_message": "Analysis complete. Found 3 potential issues..."
}

SubagentStop hooks use the same decision control format as Stop hooks.

Stop

Runs when the main Claude Code agent has finished responding. Does not run if the stoppage occurred due to a user interrupt.

Stop input

In addition to the common input fields, Stop hooks receive stop_hook_active and last_assistant_message. The stop_hook_active field is true when Claude Code is already continuing as a result of a stop hook. Check this value or process the transcript to prevent Claude Code from running indefinitely. The last_assistant_message field contains the text content of Claude’s final response, so hooks can access it without parsing the transcript file.

{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "Stop",
  "stop_hook_active": true,
  "last_assistant_message": "I've completed the refactoring. Here's a summary..."
}

Stop decision control

Stop and SubagentStop hooks can control whether Claude continues. In addition to the JSON output fields available to all hooks, your hook script can return these event-specific fields:

FieldDescription
decision"block" prevents Claude from stopping. Omit to allow Claude to stop
reasonRequired when decision is "block". Tells Claude why it should continue
{
  "decision": "block",
  "reason": "Must be provided when Claude is blocked from stopping"
}

TeammateIdle

Runs when an agent team teammate is about to go idle after finishing its turn. Use this to enforce quality gates before a teammate stops working, such as requiring passing lint checks or verifying that output files exist. When a TeammateIdle hook exits with code 2, the teammate receives the stderr message as feedback and continues working instead of going idle. TeammateIdle hooks do not support matchers and fire on every occurrence.

TeammateIdle input

In addition to the common input fields, TeammateIdle hooks receive teammate_name and team_name.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "TeammateIdle",
  "teammate_name": "researcher",
  "team_name": "my-project"
}
FieldDescription
teammate_nameName of the teammate that is about to go idle
team_nameName of the team

TeammateIdle decision control

TeammateIdle hooks use exit codes only, not JSON decision control. This example checks that a build artifact exists before allowing a teammate to go idle:

#!/bin/bash

if [ ! -f "./dist/output.js" ]; then
  echo "Build artifact missing. Run the build before stopping." >&2
  exit 2
fi

exit 0

TaskCompleted

Runs when a task is being marked as completed. This fires in two situations: when any agent explicitly marks a task as completed through the TaskUpdate tool, or when an agent team teammate finishes its turn with in-progress tasks. Use this to enforce completion criteria like passing tests or lint checks before a task can close. When a TaskCompleted hook exits with code 2, the task is not marked as completed and the stderr message is fed back to the model as feedback. TaskCompleted hooks do not support matchers and fire on every occurrence.

TaskCompleted input

In addition to the common input fields, TaskCompleted hooks receive task_id, task_subject, and optionally task_description, teammate_name, and team_name.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "TaskCompleted",
  "task_id": "task-001",
  "task_subject": "Implement user authentication",
  "task_description": "Add login and signup endpoints",
  "teammate_name": "implementer",
  "team_name": "my-project"
}
FieldDescription
task_idIdentifier of the task being completed
task_subjectTitle of the task
task_descriptionDetailed description of the task. May be absent
teammate_nameName of the teammate completing the task. May be absent
team_nameName of the team. May be absent

TaskCompleted decision control

TaskCompleted hooks use exit codes only, not JSON decision control. This example runs tests and blocks task completion if they fail:

#!/bin/bash
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject')

# Run the test suite
if ! npm test 2>&1; then
  echo "Tests not passing. Fix failing tests before completing: $TASK_SUBJECT" >&2
  exit 2
fi

exit 0

ConfigChange

Runs when a configuration file changes during a session. Use this to audit settings changes, enforce security policies, or block unauthorized modifications to configuration files. ConfigChange hooks fire for changes to settings files, managed policy settings, and skill files. The source field in the input tells you which type of configuration changed, and the optional file_path field provides the path to the changed file. The matcher filters on the configuration source:

MatcherWhen it fires
user_settings~/.claude/settings.json changes
project_settings.claude/settings.json changes
local_settings.claude/settings.local.json changes
policy_settingsManaged policy settings change
skillsA skill file in .claude/skills/ changes

This example logs all configuration changes for security auditing:

{
  "hooks": {
    "ConfigChange": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit-config-change.sh"
          }
        ]
      }
    ]
  }
}

ConfigChange input

In addition to the common input fields, ConfigChange hooks receive source and optionally file_path. The source field indicates which configuration type changed, and file_path provides the path to the specific file that was modified.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "ConfigChange",
  "source": "project_settings",
  "file_path": "/Users/.../my-project/.claude/settings.json"
}

ConfigChange decision control

ConfigChange hooks can block configuration changes from taking effect. Use exit code 2 or a JSON decision to prevent the change. When blocked, the new settings are not applied to the running session.

FieldDescription
decision"block" prevents the configuration change from being applied. Omit to allow the change
reasonExplanation shown to the user when decision is "block"
{
  "decision": "block",
  "reason": "Configuration changes to project settings require admin approval"
}

policy_settings changes cannot be blocked. Hooks still fire for policy_settings sources, so you can use them for audit logging, but any blocking decision is ignored. This ensures enterprise-managed settings always take effect.

WorktreeCreate

When you run claude --worktree or a subagent uses isolation: "worktree", Claude Code creates an isolated working copy using git worktree. If you configure a WorktreeCreate hook, it replaces the default git behavior, letting you use a different version control system like SVN, Perforce, or Mercurial. The hook must print the absolute path to the created worktree directory on stdout. Claude Code uses this path as the working directory for the isolated session. This example creates an SVN working copy and prints the path for Claude Code to use. Replace the repository URL with your own:

{
  "hooks": {
    "WorktreeCreate": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'NAME=$(jq -r .name); DIR=\"$HOME/.claude/worktrees/$NAME\"; svn checkout https://svn.example.com/repo/trunk \"$DIR\" >&2 && echo \"$DIR\"'"
          }
        ]
      }
    ]
  }
}

The hook reads the worktree name from the JSON input on stdin, checks out a fresh copy into a new directory, and prints the directory path. The echo on the last line is what Claude Code reads as the worktree path. Redirect any other output to stderr so it doesn’t interfere with the path.

WorktreeCreate input

In addition to the common input fields, WorktreeCreate hooks receive the name field. This is a slug identifier for the new worktree, either specified by the user or auto-generated (for example, bold-oak-a3f2).

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "WorktreeCreate",
  "name": "feature-auth"
}

WorktreeCreate output

The hook must print the absolute path to the created worktree directory on stdout. If the hook fails or produces no output, worktree creation fails with an error. WorktreeCreate hooks do not use the standard allow/block decision model. Instead, the hook’s success or failure determines the outcome. Only type: "command" hooks are supported.

WorktreeRemove

The cleanup counterpart to WorktreeCreate. This hook fires when a worktree is being removed, either when you exit a --worktree session and choose to remove it, or when a subagent with isolation: "worktree" finishes. For git-based worktrees, Claude handles cleanup automatically with git worktree remove. If you configured a WorktreeCreate hook for a non-git version control system, pair it with a WorktreeRemove hook to handle cleanup. Without one, the worktree directory is left on disk. Claude Code passes the path that WorktreeCreate printed on stdout as worktree_path in the hook input. This example reads that path and removes the directory:

{
  "hooks": {
    "WorktreeRemove": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'jq -r .worktree_path | xargs rm -rf'"
          }
        ]
      }
    ]
  }
}

WorktreeRemove input

In addition to the common input fields, WorktreeRemove hooks receive the worktree_path field, which is the absolute path to the worktree being removed.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "WorktreeRemove",
  "worktree_path": "/Users/.../my-project/.claude/worktrees/feature-auth"
}

WorktreeRemove hooks have no decision control. They cannot block worktree removal but can perform cleanup tasks like removing version control state or archiving changes. Hook failures are logged in debug mode only. Only type: "command" hooks are supported.

PreCompact

Runs before Claude Code is about to run a compact operation. The matcher value indicates whether compaction was triggered manually or automatically:

MatcherWhen it fires
manual/compact
autoAuto-compact when the context window is full

PreCompact input

In addition to the common input fields, PreCompact hooks receive trigger and custom_instructions. For manual, custom_instructions contains what the user passes into /compact. For auto, custom_instructions is empty.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PreCompact",
  "trigger": "manual",
  "custom_instructions": ""
}

SessionEnd

Runs when a Claude Code session ends. Useful for cleanup tasks, logging session statistics, or saving session state. Supports matchers to filter by exit reason. The reason field in the hook input indicates why the session ended:

ReasonDescription
clearSession cleared with /clear command
logoutUser logged out
prompt_input_exitUser exited while prompt input was visible
bypass_permissions_disabledBypass permissions mode was disabled
otherOther exit reasons

SessionEnd input

In addition to the common input fields, SessionEnd hooks receive a reason field indicating why the session ended. See the reason table above for all values.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "SessionEnd",
  "reason": "other"
}

SessionEnd hooks have no decision control. They cannot block session termination but can perform cleanup tasks.

Prompt-based hooks

In addition to command and HTTP hooks, Claude Code supports prompt-based hooks (type: "prompt") that use an LLM to evaluate whether to allow or block an action, and agent hooks (type: "agent") that spawn an agentic verifier with tool access. Not all events support every hook type. Events that support all four hook types (command, http, prompt, and agent):

Events that only support type: "command" hooks:

How prompt-based hooks work

Instead of executing a Bash command, prompt-based hooks:

  1. Send the hook input and your prompt to a Claude model, Haiku by default
  2. The LLM responds with structured JSON containing a decision
  3. Claude Code processes the decision automatically

Prompt hook configuration

Set type to "prompt" and provide a prompt string instead of a command. Use the $ARGUMENTS placeholder to inject the hook’s JSON input data into your prompt text. Claude Code sends the combined prompt and input to a fast Claude model, which returns a JSON decision. This Stop hook asks the LLM to evaluate whether all tasks are complete before allowing Claude to finish:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate if Claude should stop: $ARGUMENTS. Check if all tasks are complete."
          }
        ]
      }
    ]
  }
}
FieldRequiredDescription
typeyesMust be "prompt"
promptyesThe prompt text to send to the LLM. Use $ARGUMENTS as a placeholder for the hook input JSON. If $ARGUMENTS is not present, input JSON is appended to the prompt
modelnoModel to use for evaluation. Defaults to a fast model
timeoutnoTimeout in seconds. Default: 30

Response schema

The LLM must respond with JSON containing:

{
  "ok": true | false,
  "reason": "Explanation for the decision"
}
FieldDescription
oktrue allows the action, false prevents it
reasonRequired when ok is false. Explanation shown to Claude

Example: Multi-criteria Stop hook

This Stop hook uses a detailed prompt to check three conditions before allowing Claude to stop. If "ok" is false, Claude continues working with the provided reason as its next instruction. SubagentStop hooks use the same format to evaluate whether a subagent should stop:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed\n\nRespond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"your explanation\"} to continue working.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Agent-based hooks

Agent-based hooks (type: "agent") are like prompt-based hooks but with multi-turn tool access. Instead of a single LLM call, an agent hook spawns a subagent that can read files, search code, and inspect the codebase to verify conditions. Agent hooks support the same events as prompt-based hooks.

How agent hooks work

When an agent hook fires:

  1. Claude Code spawns a subagent with your prompt and the hook’s JSON input
  2. The subagent can use tools like Read, Grep, and Glob to investigate
  3. After up to 50 turns, the subagent returns a structured { "ok": true/false } decision
  4. Claude Code processes the decision the same way as a prompt hook

Agent hooks are useful when verification requires inspecting actual files or test output, not just evaluating the hook input data alone.

Agent hook configuration

Set type to "agent" and provide a prompt string. The configuration fields are the same as prompt hooks, with a longer default timeout:

FieldRequiredDescription
typeyesMust be "agent"
promptyesPrompt describing what to verify. Use $ARGUMENTS as a placeholder for the hook input JSON
modelnoModel to use. Defaults to a fast model
timeoutnoTimeout in seconds. Default: 60

The response schema is the same as prompt hooks: { "ok": true } to allow or { "ok": false, "reason": "..." } to block. This Stop hook verifies that all unit tests pass before allowing Claude to finish:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Run hooks in the background

By default, hooks block Claude’s execution until they complete. For long-running tasks like deployments, test suites, or external API calls, set "async": true to run the hook in the background while Claude continues working. Async hooks cannot block or control Claude’s behavior: response fields like decision, permissionDecision, and continue have no effect, because the action they would have controlled has already completed.

Configure an async hook

Add "async": true to a command hook’s configuration to run it in the background without blocking Claude. This field is only available on type: "command" hooks. This hook runs a test script after every Write tool call. Claude continues working immediately while run-tests.sh executes for up to 120 seconds. When the script finishes, its output is delivered on the next conversation turn:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/run-tests.sh",
            "async": true,
            "timeout": 120
          }
        ]
      }
    ]
  }
}

The timeout field sets the maximum time in seconds for the background process. If not specified, async hooks use the same 10-minute default as sync hooks.

How async hooks execute

When an async hook fires, Claude Code starts the hook process and immediately continues without waiting for it to finish. The hook receives the same JSON input via stdin as a synchronous hook. After the background process exits, if the hook produced a JSON response with a systemMessage or additionalContext field, that content is delivered to Claude as context on the next conversation turn.

Example: run tests after file changes

This hook starts a test suite in the background whenever Claude writes a file, then reports the results back to Claude when the tests finish. Save this script to .claude/hooks/run-tests-async.sh in your project and make it executable with chmod +x:

#!/bin/bash
# run-tests-async.sh

# Read hook input from stdin
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Only run tests for source files
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.js ]]; then
  exit 0
fi

# Run tests and report results via systemMessage
RESULT=$(npm test 2>&1)
EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
  echo "{\"systemMessage\": \"Tests passed after editing $FILE_PATH\"}"
else
  echo "{\"systemMessage\": \"Tests failed after editing $FILE_PATH: $RESULT\"}"
fi

Then add this configuration to .claude/settings.json in your project root. The async: true flag lets Claude keep working while tests run:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-async.sh",
            "async": true,
            "timeout": 300
          }
        ]
      }
    ]
  }
}

Limitations

Async hooks have several constraints compared to synchronous hooks:

Security considerations

Disclaimer

Command hooks run with your system user’s full permissions.

Security best practices

Keep these practices in mind when writing hooks:

Debug hooks

Run claude --debug to see hook execution details, including which hooks matched, their exit codes, and output. Toggle verbose mode with Ctrl+O to see hook progress in the transcript.

[DEBUG] Executing hooks for PostToolUse:Write
[DEBUG] Getting matching hook commands for PostToolUse with query: Write
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 hooks for query "Write"
[DEBUG] Found 1 hook commands to execute
[DEBUG] Executing hook command: <Your command> with timeout 600000ms
[DEBUG] Hook command completed with status 0: <Your stdout>

For troubleshooting common issues like hooks not firing, infinite Stop hook loops, or configuration errors, see Limitations and troubleshooting in the guide.