Reference & recipes · 2026

Claude Code Hooks

Claude Code hooks are shell commands the harness runs automatically on events — before or after a tool runs, when a session starts, or when you submit a prompt. They let you enforce policies and automate your workflow deterministically, outside the model’s judgment. Here’s every event, copy-paste recipes, and when to reach for a hook.

The hook events

Each event fires at a specific point and receives event-specific JSON on stdin.

PreToolUse

Before a tool runs. Can allow, deny, or ask, the deterministic place to block risky actions.

PostToolUse

After a tool completes. Add advisory context or run follow-ups like format-on-save or tests.

UserPromptSubmit

When you submit a prompt. Inject context or block the prompt before Claude sees it.

SessionStart

At session start, resume, or clear. Inject repo and branch context into the conversation.

SessionEnd

When a session ends. Clean up temporary state or log the session.

Stop

When the main agent finishes responding. Common for “run the test suite on stop”.

SubagentStop

When a spawned subagent finishes its work.

PreCompact

Just before Claude Code compacts the context window.

Notification

On notifications, for example when Claude needs permission or goes idle.

How a hook is built

Every hook is an entry under an event name in your settings.json. Tool events take a matcher (a regex over tool names like Write|Edit); event-only hooks like Stop omit it and always fire.

{
  "hooks": {
    "<EventName>": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "your-script.sh" }
        ]
      }
    ]
  }
}

The command receives the event JSON on stdin (read a field with jq -r '.tool_input.file_path'). Exit 0 to continue; on a PreToolUse hook, exit 2 to block the action and send your stderr back to Claude as the reason.

Copy-paste recipes

Six hooks worth stealing, each a complete block you can drop straight into your settings.json. Have more than one? Merge them under a single "hooks" key.

PostToolUse

Format every file the moment Claude edits it

Runs Prettier on whatever file a Write or Edit touched, so the tree stays formatted without you asking. The file path arrives on stdin as tool_input.file_path.

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

Run your tests the moment Claude stops

Fires once the main agent finishes responding. A fast way to catch a regression before you even read the diff. Stop takes no matcher, it always fires.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "npm test --silent"
          }
        ]
      }
    ]
  }
}
PreToolUse

Never let Claude touch your .env

Blocks any Write or Edit whose path contains .env and tells Claude why. Exit code 2 on a PreToolUse hook is a hard deny the model cannot override, and the message on stderr is fed back so it understands the refusal.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "f=$(jq -r '.tool_input.file_path'); case $f in *.env|*.env.*) echo 'Refusing to modify .env files.' >&2; exit 2;; esac"
          }
        ]
      }
    ]
  }
}
PreToolUse

Stop a recursive force-delete before it runs

Inspects the Bash command on stdin and blocks rm -rf (and rm -fr) with exit 2. Everything else passes straight through. A safety net for the one command you never want run unattended.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' | grep -qE 'rm -(rf|fr)' && { echo 'Blocked a recursive force-delete. Run it yourself if you really mean it.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}
SessionStart

Greet every session with your git state

Prints the current branch and how many files are uncommitted. A SessionStart hook that exits 0 has its stdout injected straight into the conversation, so Claude starts every session knowing where the repo stands.

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo Branch $(git branch --show-current 2>/dev/null) has $(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') uncommitted files"
          }
        ]
      }
    ]
  }
}
Notification

Ping your desktop when Claude needs you

Surfaces a native notification when Claude asks for permission or goes idle. The event type arrives on stdin as notification_type. macOS shown; on Linux swap terminal-notifier for notify-send.

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "terminal-notifier -title 'Claude Code' -message $(jq -r .notification_type) 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

Hook, skill, or subagent?

The three ways to change Claude Code’s behavior are not interchangeable:

Hook enforces the rule, deterministic, harness-run, regardless of what the model decides.
Skill teaches the how, on-demand expertise, loaded only when relevant.
Subagent isolates the work, a separate context with its own restricted tools.

Full breakdown in the anatomy of a 2026 Claude Code plugin.

Plugins that ship hooks

See advisory, fail-safe hooks in action:

Frequently asked questions

What are Claude Code hooks?

Hooks are shell commands the Claude Code harness runs automatically on events, before or after a tool runs, at session start, when you submit a prompt, and more. Because the harness runs them (not the model), they fire deterministically every time, which makes them ideal for enforcing policies and automating your workflow.

What events can a hook listen to?

PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop, SubagentStop, PreCompact, and Notification. Each event carries event-specific JSON on stdin, the tool name, tool input and output, the prompt, or session info.

How do I add a hook?

Add a hooks block to your settings.json (user or project scope), or ship a hooks/hooks.json file inside a plugin. Each entry has a matcher (for example "Edit|Write") and a command to run. Plugins reference bundled scripts with ${CLAUDE_PLUGIN_ROOT} so they resolve wherever the plugin is installed.

How do I format code automatically after Claude edits a file?

Use a PostToolUse hook matched to "Write|Edit" that reads the edited path from stdin (jq -r '.tool_input.file_path') and pipes it to your formatter, for example xargs npx prettier --write. It runs after every edit, so the tree stays formatted without you asking. There is a copy-paste version in the recipes above.

How do I stop Claude from editing certain files, like .env or secrets?

Use a PreToolUse hook matched to "Write|Edit" that checks the file path on stdin and exits with code 2 when it matches a file you want protected. Exit 2 is a hard deny the model cannot override, and the message you print to stderr is fed back so Claude understands the refusal. See the "Never let Claude touch your .env" recipe.

How do I run my tests automatically when Claude finishes?

Add a Stop hook (no matcher, it always fires) whose command runs your test script, for example npm test --silent. It runs the moment the main agent stops responding, so a regression surfaces before you read the diff.

Can a hook block a tool call?

Yes. A PreToolUse hook can deny a tool (exit code 2, or a permissionDecision of "deny"), which is how you enforce hard rules like “never write outside the repo” or “never commit a secret”. Exit 0 with additionalContext is advisory, it surfaces a suggestion to Claude without blocking.

Where do plugin hooks live?

In hooks/hooks.json at the plugin root, which Claude Code auto-detects. The command references bundled scripts via ${CLAUDE_PLUGIN_ROOT} so they resolve to wherever the plugin is installed.

Are hooks safe?

Hooks run arbitrary commands with your permissions, so only install hooks you trust. Well-built plugin hooks are fail-safe (any error exits 0 and does nothing) and advisory by default, so they surface a suggestion without ever blocking your work or breaking a session.

Do hooks slow down Claude Code?

A well-written hook is near-instant. Keep hook commands fast and fail-safe; heavy or long-running work belongs in a slash command or a subagent, not a hook.

Automate your workflow with hooks

Browse plugins that ship ready-made hooks, or read the full setup guide.

Read the guide