Skip to content

Hooks ​

Hooks run shell commands at fixed points in Claude Code's lifecycle. Unlike CLAUDE.md instructions, which Claude may follow, hooks are deterministic — they execute every time, regardless of what the model decides. Use them when something must always happen: format after an edit, block writes to a protected path, validate a prompt.

Shape in settings.json ​

Hooks live under the hooks key, grouped by event, each with a matcher and a list of handlers:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "prettier --write \"$file\"", "timeout": 30 }
        ]
      }
    ]
  }
}

The command handler is the most common. Other handler types exist (http, prompt, agent, mcp_tool); verify the current set in the docs before relying on them.

Events ​

EventFires whenCan block?
PreToolUseBefore a tool runsYes
PostToolUseAfter a tool succeedsNo
UserPromptSubmitYou submit a promptYes
SessionStartSession begins (startup / resume / clear / compact)No
SessionEndSession endsNo
StopClaude finishes respondingYes
SubagentStopA subagent finishesYes
PreCompactBefore context compaction (manual / auto)Yes
NotificationClaude emits a notificationNo

NOTE

Claude Code defines additional events beyond these core ones. Check the latest docs for the full table and which events support blocking.

Matchers ​

For tool events, matcher filters by tool name: Bash, Edit|Write (pipe-separated), mcp__.* (regex). MCP tools are named mcp__<server>__<tool>. A "*", "", or omitted matcher fires on every occurrence. Non-tool events match other sources — e.g. SessionStart matches startup / resume / clear / compact.

Exit codes ​

A command hook signals its result through its exit code. The exact effect depends on whether the event can block (see the table above):

Exit codeMeaning
0Success. On UserPromptSubmit and SessionStart, stdout is added to Claude's context; for other events stdout goes to the debug log.
2Block — but only on blocking-capable events (e.g. PreToolUse, Stop). stderr is fed back to Claude. On non-blocking events like PostToolUse the action already ran, so exit 2 doesn't undo it; stderr is shown to Claude as context.
otherNon-blocking error; stderr goes to the debug log.

NOTE

A hook can also return structured JSON on stdout (exit 0) for finer control. The fields and which events honor them are documented at the source below — check it before relying on a specific behavior.

Example: format on edit ​

Run a formatter automatically after every Edit or Write:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -r prettier --write",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

The hook receives a JSON payload on stdin; here jq extracts the edited file path and pipes it to prettier.

Example: guardrail with exit code 2 ​

Block edits to a protected path before they happen. Save as guard.sh:

bash
#!/usr/bin/env bash
path=$(jq -r '.tool_input.file_path')
if [[ "$path" == *".env"* ]]; then
  echo "Refusing to modify secret file: $path" >&2
  exit 2   # blocks the tool call; stderr is shown to Claude
fi
exit 0

Wire it to PreToolUse:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/guard.sh" }
        ]
      }
    ]
  }
}

Because PreToolUse can block, exiting 2 cancels the edit and tells Claude why.

TIP

Hooks are configured in settings.json, so they follow the same scope precedence. Ready-made hook scripts live in assets/hooks/.

WARNING

Hooks run with your shell's privileges. Review any hook before adding it, especially from untrusted sources.

Source: code.claude.com/docs/en/hooks

Released under the MIT License.