Skip to main content
Hooks let you attach custom logic to specific lifecycle events in Claude Code. When a hook event fires, Claude Code runs the configured command, evaluates a prompt, or spawns a sub-agent — before or after the triggering action takes place.

Hook events

Each hook is registered under one of the following events:
EventWhen it fires
PreToolUseBefore a tool is invoked
PostToolUseAfter a tool returns successfully
PostToolUseFailureAfter a tool returns an error
NotificationWhen Claude Code sends a desktop notification
UserPromptSubmitWhen the user submits a message
SessionStartAt the start of a new session
SessionEndWhen the session ends
StopWhen the agent finishes its response
StopFailureWhen the agent stops due to an error
SubagentStartWhen a sub-agent is spawned
SubagentStopWhen a sub-agent finishes
PreCompactBefore context compaction
PostCompactAfter context compaction
PermissionRequestWhen a permission check is triggered
PermissionDeniedWhen a permission is denied
SetupOn init or maintenance trigger
ConfigChangeWhen settings change on disk
InstructionsLoadedWhen a CLAUDE.md file is loaded
CwdChangedWhen the working directory changes
FileChangedWhen a watched file changes

Configuration

Hooks are defined in your settings JSON file (e.g. ~/.claude/settings.json for user-level hooks, or .claude/settings.json for project-level hooks) under the hooks key. The value is a map from event name to an array of matcher configurations.
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'About to run Bash'"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude finished'"
          }
        ]
      }
    ]
  }
}
Each matcher object has:
  • matcher (optional) — a string pattern matched against the tool name (e.g. "Bash", "Write")
  • hooks — an array of hook definitions to run when the matcher matches

The if condition

Every hook supports an if field that uses permission rule syntax to filter when the hook runs. The condition is evaluated against the hook input’s tool_name and tool_input fields.
{
  "type": "command",
  "command": "./scripts/lint.sh",
  "if": "Bash(git *)"
}
Only calls where the tool matches the pattern will trigger the hook. This avoids spawning hooks for non-matching commands. Examples:
  • "Bash(git *)" — any Bash call starting with git
  • "Read(*.ts)" — any Read call on a .ts file
  • "Write" — any Write call, regardless of path

Hook types

Runs a shell command. The hook input is passed as JSON via stdin.
{
  "type": "command",
  "command": "python3 ~/hooks/verify.py",
  "if": "Bash(git commit*)",
  "shell": "bash",
  "timeout": 30,
  "statusMessage": "Verifying commit...",
  "once": false,
  "async": false,
  "asyncRewake": false
}
FieldTypeDescription
commandstringShell command to execute
ifstringPermission rule filter (optional)
shell"bash" | "powershell"Shell interpreter. Defaults to bash (uses $SHELL)
timeoutnumberTimeout in seconds for this command
statusMessagestringCustom spinner message while the hook runs
oncebooleanIf true, the hook is removed after its first execution
asyncbooleanIf true, runs in the background without blocking the agent
asyncRewakebooleanRuns in the background; wakes the model if exit code is 2 (implies async)
Exit code behaviour:
  • 0 — success, continue normally
  • 2 (with asyncRewake: true) — the model is woken and given the hook’s stdout as a new message
  • Any other non-zero — the hook is treated as an error
Sends a prompt to an LLM. Use $ARGUMENTS in the prompt string to inject the hook input JSON.
{
  "type": "prompt",
  "prompt": "Review the following tool input and check for secrets or credentials. Respond with 'BLOCK' if you find any, otherwise 'OK'. Input: $ARGUMENTS",
  "if": "Write",
  "model": "claude-sonnet-4-6",
  "timeout": 20,
  "statusMessage": "Checking for secrets...",
  "once": false
}
FieldTypeDescription
promptstringPrompt to evaluate. Use $ARGUMENTS for hook input JSON
ifstringPermission rule filter (optional)
modelstringModel to use (e.g. "claude-sonnet-4-6"). Defaults to the small fast model
timeoutnumberTimeout in seconds
statusMessagestringCustom spinner message
oncebooleanIf true, removed after first execution
Spawns a sub-agent to verify or act on the hook event. The agent receives a prompt describing what to check, with $ARGUMENTS replaced by the hook input JSON.
{
  "type": "agent",
  "prompt": "Verify that unit tests ran and passed before this commit. $ARGUMENTS",
  "if": "Bash(git commit*)",
  "model": "claude-sonnet-4-6",
  "timeout": 60,
  "statusMessage": "Running verification agent...",
  "once": false
}
FieldTypeDescription
promptstringVerification prompt. Use $ARGUMENTS for hook input JSON
ifstringPermission rule filter (optional)
modelstringModel for the agent (e.g. "claude-sonnet-4-6"). Defaults to Haiku
timeoutnumberTimeout in seconds (default 60)
statusMessagestringCustom spinner message
oncebooleanIf true, removed after first execution
POSTs the hook input JSON to a URL. Useful for integrating with external systems or webhooks.
{
  "type": "http",
  "url": "https://hooks.example.com/claude-event",
  "if": "PostToolUse",
  "timeout": 10,
  "headers": {
    "Authorization": "Bearer $MY_TOKEN",
    "Content-Type": "application/json"
  },
  "allowedEnvVars": ["MY_TOKEN"],
  "statusMessage": "Notifying webhook...",
  "once": false
}
FieldTypeDescription
urlstringURL to POST the hook input JSON to
ifstringPermission rule filter (optional)
timeoutnumberTimeout in seconds
headersRecord<string, string>Additional HTTP headers. Values can reference env vars using $VAR_NAME
allowedEnvVarsstring[]Env var names allowed to be interpolated in header values
statusMessagestringCustom spinner message
oncebooleanIf true, removed after first execution

Background execution

By default hooks run synchronously and block the agent until they complete. Use async to run a hook in the background:
{
  "type": "command",
  "command": "~/scripts/log-tool-use.sh",
  "async": true
}
If you also want the background hook to be able to wake the model — for example to report a problem discovered asynchronously — use asyncRewake:
{
  "type": "command",
  "command": "~/scripts/monitor.sh",
  "asyncRewake": true
}
When the command exits with code 2, Claude Code treats the stdout as a new user message and resumes the model. Any other exit code is treated as normal background completion.

Full example

The following settings configuration uses all four hook types across multiple events:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo '[hook] PreToolUse Bash' >> /tmp/claude-hooks.log",
            "async": true
          },
          {
            "type": "prompt",
            "prompt": "Check the following Bash command for dangerous operations like rm -rf or curl | sh. Say BLOCK if it should be blocked, otherwise OK. Command: $ARGUMENTS",
            "if": "Bash",
            "statusMessage": "Safety checking command..."
          }
        ]
      },
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify the file being written does not contain hardcoded API keys or passwords. $ARGUMENTS",
            "statusMessage": "Scanning for credentials..."
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "https://hooks.example.com/post-tool",
            "headers": { "Authorization": "Bearer $WEBHOOK_TOKEN" },
            "allowedEnvVars": ["WEBHOOK_TOKEN"],
            "async": true
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'",
            "async": true
          }
        ]
      }
    ]
  }
}
Hook scripts receive the full hook input as JSON on stdin. For PreToolUse and PostToolUse events this includes tool_name, tool_input, session_id, transcript_path, and cwd.

Build docs developers (and LLMs) love