Hooks

Status: 🟩 COMPLETE (🟦 LIVING) Last updated: 2026-06-19 Plain-English tagline: Shell commands that run automatically when Claude Code does specific things — before tool use, after a session ends, when a task completes. The way you enforce non-negotiable behavior.


In plain English

Most of Claude’s behavior is soft — the model decides what to do based on its prompt and your message. That’s powerful but probabilistic. Sometimes you want behavior that’s hard — must happen, every time, regardless of what the model chooses.

Hooks are how you do that. A hook is a shell command (or shell script) that Claude Code runs automatically on a specific event:

  • Before a tool runs (PreToolUse)
  • After a tool runs (PostToolUse)
  • When Claude finishes a turn (Stop)
  • When you submit a message (UserPromptSubmit)
  • When the session starts (SessionStart)
  • And several others

The hook command can do anything a shell command can: write to a log file, send a desktop notification, format a file, run a linter, validate an action and block it. Because hooks are shell scripts, they’re deterministic — they can’t hallucinate, they always run, they always produce a definite outcome.


Why it matters

Three reasons:

  1. Hard guarantees. “Never let Claude run rm -rf outside this folder” is a rule you can’t enforce with prompts — Claude might forget or be tricked. A hook enforces it absolutely.

  2. Automation glue. “After Claude edits a file, run prettier on it” — a hook makes this automatic, so you don’t have to remember.

  3. Observability. Hooks can log every action Claude takes. Useful for review, audit, replay.

For solo dev work, a few well-chosen hooks raise the floor of your Claude Code experience considerably. For team or production use, hooks are how you bake in compliance and safety.


The hook events

The most useful events you can hook into:

EventFires whenCommon use
PreToolUseJust before any tool call runsValidate or block the action
PostToolUseAfter a tool call completesLogging, formatting, follow-up
StopWhen Claude finishes respondingDesktop notification when long task done
UserPromptSubmitYou hit enter on a messagePre-processing the prompt
SessionStartAt the beginning of a sessionSetup, environment checks
SessionEndWhen you close the sessionCleanup, save state
NotificationWhen Claude needs your attentionTrigger sounds, push to phone

A hook can be filtered by matcher — e.g. only fire on Write/Edit tools, only on specific commands, only on certain matchers. This keeps hooks scoped.


How hooks are configured

Hooks live in .claude/settings.json (either global at ~/.claude/settings.json or project-local at <project>/.claude/settings.json).

The shape:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "echo 'about to write a file' >> ~/claude-log.txt" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "notify-send 'Claude is done'" }
        ]
      }
    ]
  }
}

Each event maps to an array of matcher-grouped hook lists. The matcher is a regex (for events that have a “tool name” to match against). If omitted, the hook fires for every occurrence of that event.


Blocking actions with PreToolUse

A PreToolUse hook can block the tool call by exiting with a non-zero status code. The tool call is canceled and Claude sees the hook’s stderr output as the reason.

Example: block rm -rf from being run anywhere outside /tmp:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -E 'rm -rf' | grep -v '/tmp'; then echo 'rm -rf outside /tmp is forbidden' >&2; exit 1; fi"
        }
      ]
    }
  ]
}

The hook receives the planned tool call via environment variables (CLAUDE_TOOL_INPUT contains the arguments). If it exits 1, Claude doesn’t run the command, sees the error message, and can react (usually by trying a different approach or asking you).

This is how you build hard guardrails that the model literally cannot bypass.


A handful of useful hook recipes

1. Desktop notification when Claude finishes

Useful for long-running tasks so you can walk away:

{
  "Stop": [
    {
      "hooks": [
        { "type": "command", "command": "powershell -Command \"[System.Console]::Beep(800, 200)\"" }
      ]
    }
  ]
}

On Windows. For macOS, osascript -e 'display notification \"Claude done\" with title \"Claude Code\"'. On Linux, notify-send.

2. Auto-format after edits

{
  "PostToolUse": [
    {
      "matcher": "Edit|Write",
      "hooks": [
        { "type": "command", "command": "npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true" }
      ]
    }
  ]
}

Runs prettier on any file Claude edits. The || true ensures the hook doesn’t fail if prettier isn’t installed.

3. Log every tool call to a file

{
  "PreToolUse": [
    {
      "hooks": [
        { "type": "command", "command": "echo \"$(date -Iseconds) $CLAUDE_TOOL_NAME\" >> ~/.claude/tool-use.log" }
      ]
    }
  ]
}

Useful for retroactive review of what Claude has done.

4. Block secrets from being written

{
  "PreToolUse": [
    {
      "matcher": "Write|Edit",
      "hooks": [
        { "type": "command", "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'sk-[a-zA-Z0-9]{40}|eyJ[A-Za-z0-9_-]{100,}'; then echo 'looks like a secret — blocked' >&2; exit 1; fi" }
      ]
    }
  ]
}

Crude but catches obvious leakage of API keys / tokens into committed files.

5. Run a smoke check before commits

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        { "type": "command", "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'git commit'; then npm run typecheck && npm run lint || { echo 'pre-commit checks failed'; exit 1; }; fi" }
      ]
    }
  ]
}

Blocks commits if typecheck or lint fails.


Hooks vs git hooks

Both run shell commands on events; the events are different.

  • Git hooks (.git/hooks/pre-commit, etc.) run when git operations happen — independent of Claude Code.
  • Claude Code hooks run when Claude operates — independent of git.

A git commit from Claude Code triggers both: a PreToolUse Claude hook on the Bash call, then a pre-commit git hook inside git. They compose; use whichever scope fits.

Git hooks are usually a better fit for “things that must happen regardless of who commits” (CI-style checks). Claude Code hooks are better for “things specific to AI-driven work” (logging Claude’s tool calls, blocking patterns the model is prone to).


How hook output reaches you

  • stdout of a hook is appended to Claude’s view of what just happened — Claude sees it as context.
  • stderr of a hook is shown to you and (if non-zero exit) to Claude as the blocking reason.
  • Exit code 0 means “all good, proceed.”
  • Non-zero exit in PreToolUse means “block this action.”

You can also produce structured output (JSON to stdout) that Claude can parse — useful for richer hook behavior.


Performance considerations

Hooks run synchronously. A slow hook slows down Claude. Two implications:

  1. Heavy hooks (running tests, calling APIs) on PreToolUse make every tool call slow. Restrict the matcher so they only run on relevant tools.
  2. For long-running work, fire-and-forget into the background (& on Unix) — but then you lose the ability to block.

Most hooks should run in under 50ms. If yours takes longer, consider whether PostToolUse is the right event (so it doesn’t block the chain).


Common gotchas

  • Hook scripts are shell scripts. Quoting, escaping, env-var expansion — all the usual shell traps apply. Test the script outside Claude before wiring it as a hook.

  • Cross-platform pain. A hook that uses notify-send works on Linux but not Windows. A hook with powershell works on Windows but not Linux. For portable hooks, write a small script that detects the OS and dispatches.

  • A non-zero exit in PreToolUse blocks the action — but only that action. Claude can try something else. The hook doesn’t end the session.

  • Matchers are regex. If your matcher accidentally matches everything (e.g. .* or an unanchored common word), the hook fires on every tool call. Anchor matchers carefully.

  • Hooks see what Claude planned to do, not what happened. PreToolUse shows the intended call. PostToolUse shows the result. Don’t mix them up.

  • Don’t put credentials in hook commands. They get logged. Use env vars or files.

  • Loops are possible. A PostToolUse hook that triggers another tool call can recurse. Be careful with hooks that themselves invoke Claude.

  • Hooks run with your shell environment. They see your PATH, your env vars, your aliases (or not, depending on the shell). Test them in the actual environment Claude runs them in.

  • Debugging hooks is hard. Write logs to a file (echo X >> ~/hook.log) so you can inspect what happened after a session.

  • Some hook events are still experimental. Behavior may change. Pin Claude Code versions for production hook reliance.


See also


Sources