Claude Code Hooks (2026): Every Hook Event, 6 Examples and the Gotchas

by Sandlabs Team, Founder, Sandlabs

Every team that adopts Claude Code hits the same wall around week three. Claude is fast — too fast. It writes code, edits files, runs commands, and you find yourself trailing behind it: re-running the formatter, scrolling back to check what got committed, asking "did it run the tests?" five times a day.

The fix is hooks. Claude Code hooks let you run shell commands automatically when specific events happen — before a tool runs, after a file is written, when Claude finishes responding. Used well, they replace the running mental checklist with deterministic guarantees: every edit gets formatted, every Bash command gets logged, every push to main gets blocked unless you approve it.

This guide is the practical version. What hooks actually are, the events you'll use, the JSON syntax, a full reference table of every hook event, and six hooks we run on real consulting projects (including a Stop hook). Current as of October 2026, checked against Anthropic's hooks reference.

What Are Claude Code Hooks?

Claude Code hooks are user-defined shell commands that fire at specific points in Claude's execution lifecycle — before/after tool calls, on session start, when Claude stops, when a notification appears, and others. They're configured in settings.json and run as ordinary shell processes with structured JSON input on stdin.

The mental model: hooks are deterministic rules. Slash commands and prompts ask Claude to do something. Hooks just happen. If you write a PostToolUse hook for Edit|Write that runs Prettier, every Edit and Write — across every conversation, every project, every developer on the team — runs Prettier afterward. There is no "Claude forgot."

This is the feature that turns Claude Code from a clever pair-programmer into something you can deploy across a team without losing sleep over consistency.

The Hook Events You'll Actually Use

Claude Code currently documents 33 hook events (the full list is below). You'll only need a handful in practice. The most useful for day-to-day team workflows:

  • PreToolUse — fires before any tool runs. Use to block, ask, or transform tool input. The most powerful event; this is where guardrails live.
  • PostToolUse — fires after a tool succeeds. Use for auto-formatting, audit logging, or running tests after writes.
  • UserPromptSubmit — fires when the user submits a prompt, before Claude processes it. Use to inject context or validate input.
  • SessionStart — fires when a session opens or resumes. Use to load env vars, inject project state, or warm up context.
  • Stop — fires when Claude finishes responding. Use to validate output or force a continuation if a check fails.
  • Notification — fires when Claude needs attention (permission prompt, idle). Use for desktop notifications.
  • PreCompact / PostCompact — fires around context compaction. Use to snapshot or re-inject critical context.
  • SubagentStop — fires when a subagent finishes. Use to capture results or trigger follow-up work.

Newer events like PostToolBatch, Elicitation, TaskCompleted, and WorktreeCreate cover more advanced workflows (parallel tool calls, MCP input interception, multi-agent coordination). Start with PreToolUse and PostToolUse — they cover 80% of the value.

Every Claude Code Hook Event

The complete list as of October 2026, from Anthropic's hooks reference. "Matcher" is what the matcher field is compared against; "Exit 2" is what happens when your hook exits with code 2.

EventFires whenMatcher matchesExit 2 does
SessionStartA session starts or resumesstartup, resume, clear, compact, forkShows stderr to the user only
SetupClaude Code runs an init or maintenance passinit, maintenanceIgnored
UserPromptSubmitYou submit a prompt, before Claude sees itNo matcherBlocks the prompt
UserPromptExpansionA typed command expands into a promptCommand nameBlocks the expansion
PreToolUseBefore any tool call runsTool nameBlocks the tool call
PermissionRequestA tool call needs a permission decisionTool nameNot honoured; use the JSON decision instead
PermissionDeniedAuto mode denies a tool callTool nameIgnored
PostToolUseA tool call succeedsTool nameShows stderr to Claude
PostToolUseFailureA tool call failsTool nameShows stderr to Claude
PostToolBatchA batch of parallel tool calls resolvesNo matcherStops the agentic loop
NotificationClaude Code sends a notificationNotification typeIgnored
MessageDisplayClaude's text is shown to youNo matcherOnly changes what is displayed
SubagentStartA subagent is spawnedAgent typeCan add context
SubagentStopA subagent finishesAgent typeKeeps the subagent working
TaskCreatedA task is createdNo matcherRolls back the task
TaskCompletedA task is marked completeNo matcherPrevents completion
StopClaude finishes respondingNo matcherKeeps Claude working
StopFailureA turn ends on an API errorError typeIgnored
TeammateIdleAn agent-team teammate is about to go idleNo matcherKeeps it working
InstructionsLoadedCLAUDE.md or a rules file is loadedLoad reasonIgnored
ConfigChangeA config file changes mid-sessionSettings sourceBlocks the change (except managed policy)
CwdChangedThe working directory changesNo matcherShows stderr to the user
DirectoryAddedA directory is added with /add-dirHow it was addedLogged only
FileChangedA watched file changesFile namesShows stderr to the user
WorktreeCreateA worktree is being createdNo matcherAny non-zero exit fails creation
WorktreeRemoveA hook-created worktree is removedNo matcherCan fail removal
PreCompactBefore context compactionmanual, autoBlocks compaction
PostCompactAfter context compactionmanual, autoShows stderr to the user
PreModelSwitchBefore the model changesModel nameBlocks the switch
PostModelSwitchAfter the model changesModel nameShows stderr to the user
ElicitationAn MCP server asks you for inputMCP server nameDeclines the request
ElicitationResultYou answer an MCP elicitationMCP server nameBlocks the answer
SessionEndThe session endsEnd reasonShows stderr to the user

Two things the table makes obvious: only a few events can actually stop something (PreToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact, ConfigChange), and the rest are for logging, notifying or injecting context.

Anatomy of a Hook

Hooks live in ~/.claude/settings.json (global), .claude/settings.json (project, committed to the repo), or .claude/settings.local.json (project, gitignored). The structure:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_PROJECT_DIR\"",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Three things to understand:

1. The matcher. A pattern that decides which tool calls trigger the hook. "" or omitted matches every occurrence. Edit|Write matches both. Bash matches only Bash. mcp__github__.* matches all GitHub MCP tools. A matcher made only of letters, digits, _, -, spaces, commas and | is an exact match; any other character turns it into a regular expression. Matchers are case-sensitive.

2. Hook input. Your command receives a JSON payload on stdin: session_id, cwd, hook_event_name, tool_name, tool_input, and event-specific fields. Parse it with jq or your language of choice.

3. Hook output. Exit codes control behaviour:

  • exit 0 — proceed (and stdout becomes additional context for some events)
  • exit 2 — block the action (stderr is shown to Claude or the user)
  • other codes — non-blocking error, logged

For richer control, exit 0 with a structured JSON object on stdout. Set hookSpecificOutput.permissionDecision to "allow", "deny", or "ask" to override the default permission flow.

Handler types. "type": "command" (a shell command) is the one you'll use most, but hooks can also be http (POST the event to a URL), mcp_tool (call an MCP tool), prompt (ask a model to decide) or agent (experimental). Type /hooks inside Claude Code to see every hook that is active and which settings file it came from.

Six Claude Code Hook Examples That Earn Their Keep

These are pulled from real Sandlabs client projects. Drop them into .claude/settings.json and they work immediately.

1. Auto-format every file Claude touches

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -I {} sh -c 'npx prettier --write {} 2>/dev/null || true'"
          }
        ]
      }
    ]
  }
}

Saves you from PRs that mix Claude's output with manual formatter passes. Works for TypeScript, JSON, YAML, Markdown — anything Prettier handles. The || true keeps Claude moving if Prettier doesn't recognise a file type.

2. Block writes outside the project root

Save this as .claude/hooks/project-boundary.sh:

#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -n "$FILE" && "$FILE" != "$CLAUDE_PROJECT_DIR"* ]]; then
  echo "Blocked: writes outside $CLAUDE_PROJECT_DIR are not allowed" >&2
  exit 2
fi
exit 0

Wire it up:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/project-boundary.sh"
          }
        ]
      }
    ]
  }
}

Critical when Claude is working in a client repo that lives next to your home dotfiles. One stray edit to ~/.zshrc ruins the day.

3. Audit log every Bash command

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{ts: (now | todate), cwd, cmd: .tool_input.command}' >> \"$HOME\"/.claude/audit.jsonl"
          }
        ]
      }
    ]
  }
}

For regulated environments, this is non-negotiable. JSONL is grep-friendly and works with jq for analysis. Pair with a weekly cron that ships the file to S3, BigQuery, or your SIEM of choice.

4. Desktop notification when Claude is waiting

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude needs your input\" with title \"Claude Code\" sound name \"Glass\"'"
          }
        ]
      }
    ]
  }
}

macOS version. Linux: replace with notify-send "Claude Code" "Claude needs your input". Stops you from staring at the terminal between answers — useful when Claude is running long agentic tasks and only occasionally needs a permission decision.

5. Force approval on destructive git commands

.claude/hooks/git-safety.sh:

#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qE 'git (push.*--force|reset --hard|clean -[fd]+|branch -D)'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "ask",
      permissionDecisionReason: "Destructive git command — confirm before running."
    }
  }'
fi
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/git-safety.sh"
          }
        ]
      }
    ]
  }
}

We added this after a session force-pushed over three days of in-progress work on a client repo. Cheap insurance for shared codebases.

6. Stop hook — don't let Claude stop until the types compile

A Stop hook fires when Claude finishes responding. If it exits 2, Claude is forced to keep working — which makes the Stop hook perfect for "don't stop until the build is green." The catch: you must check stop_hook_active and exit cleanly when it is already set, or you will loop forever.

Save this as .claude/hooks/stop-until-green.sh:

#!/bin/bash
INPUT=$(cat)
# Already re-prompted once this turn — let Claude stop to avoid an infinite loop
if [[ "$(echo "$INPUT" | jq -r '.stop_hook_active')" == "true" ]]; then
  exit 0
fi
if ! npm run -s typecheck >/dev/null 2>&1; then
  echo "Typecheck is failing — fix the errors before you stop." >&2
  exit 2
fi
exit 0

Wire it up:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-until-green.sh"
          }
        ]
      }
    ]
  }
}

This turns "Claude said it's done" into "Claude is done and the types compile." The stop_hook_active guard is the entire difference between a useful gate and an infinite loop — the most common mistake people make with the Claude Code Stop hook.

How to Stop Claude Reading .env Files

This one comes up in Anthropic's course material as a quiz question: a developer wants to prevent Claude from reading sensitive .env files. Which type of hook should they set up, and what tool names would they likely match?

The answer is a PreToolUse hook matching Read and Grep. It has to be PreToolUse because only a pre-tool hook can stop the call before the file is opened; a PostToolUse hook fires after Claude has already seen the contents. And it matches the tools that read (Read, Grep), not the ones that write or delete.

In a real project, go further than the quiz answer:

{
  "permissions": {
    "deny": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)"]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Grep|Glob|Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-env.sh" }
        ]
      }
    ]
  }
}

Your block-env.sh reads the tool input from stdin, checks whether the path or command mentions .env, and exits 2 with a reason on stderr if it does.

Three caveats from Anthropic's docs that most guides miss:

  • @ file mentions skip tools entirely. If you type @.env, no tool call happens, so no PreToolUse hook fires. The Read(...) deny rule is what catches that case.
  • Deny rules are the primary control; the hook is the extra layer. Claude applies Read deny rules to Grep, Glob and @ mentions, and to common shell readers like cat, head and tail.
  • Neither catches everything in Bash. A script that opens the file itself, or a recursive grep, can get past both. For real secrets, turn on Claude Code's sandbox, and keep production secrets out of the repo altogether.

Common Gotchas

A few things that will trip you up the first week:

Exit codes vs JSON output, not both. If you exit 2, Claude reads stderr and ignores stdout. If you exit 0 with JSON, Claude parses stdout. Mixing them leaves you debugging silent failures.

The cwd in hook input is not your script's working directory. Hook input gives you cwd as Claude saw it. Your script's actual cwd may differ. Always use $CLAUDE_PROJECT_DIR for project paths.

Matchers are case-sensitive. bash does not match Bash. Use exact tool names.

Stop hooks can loop forever. A Stop hook that exits 2 forces Claude to keep going. If your script doesn't check stop_hook_active from the input JSON and early-exit when it's true, you'll spin until the timeout.

Make scripts executable. chmod +x .claude/hooks/*.sh on macOS and Linux. Windows ignores this.

Don't store secrets in hooks. Hooks run in your shell with full environment access. They are powerful — and dangerous. Review every hook a teammate adds, especially in shared .claude/settings.json files. Treat hooks like CI scripts, not config.

Hooks vs Slash Commands vs MCP Servers

Teams reach for the wrong tool here all the time:

  • Hooks — deterministic, always-run, event-driven. Use when you need a rule that fires every time without asking.
  • Slash commands — Claude-invoked, contextual, judgment-dependent. Use when you want Claude to decide whether and how to run something.
  • MCP servers — entire tool ecosystems Claude can call into. Use when you need a rich, reusable integration (GitHub, Slack, your database). See our Notion MCP server guide and best MCP servers 2026 for examples.

Rule of thumb: if it must always happen, it's a hook. If Claude should decide, it's a slash command or an MCP tool.

Frequently Asked Questions

Can Claude Code hooks run on Windows?

Yes. The hook command runs in your default shell. On Windows, that's PowerShell or cmd; use platform-appropriate commands (notify-send won't work, New-BurntToastNotification will). Most teams settle on cross-platform tooling like Node.js or Python scripts inside their hooks to avoid maintaining three versions.

Do hooks slow Claude down?

A simple hook (formatter, log append) adds 50–500ms per tool call. Heavy hooks (running a test suite) can add seconds. Use the timeout field to cap them. For very expensive checks, run them asynchronously or move them to CI rather than blocking Claude's session.

Can I use hooks to enforce security policy across a team?

Yes — that is the strongest use case. Commit .claude/settings.json to the repo with hooks that block dangerous tool calls: writes outside the repo, force pushes, edits to .env files. Every developer who clones the repo inherits the policy automatically. Pair this with code review on changes to the hook scripts themselves.

What happens if a hook errors?

Exit codes other than 0 or 2 are logged but non-blocking — Claude continues. If you need an error to halt the action, exit 2 explicitly. Use set -e and set -u in shell hook scripts to surface bugs early during development.

Are Claude Code hooks the same as Git hooks?

No. Git hooks fire on Git operations (commit, push). Claude Code hooks fire on Claude's tool calls and lifecycle events. They are complementary — Git hooks for repo policy, Claude Code hooks for AI agent behaviour.

Which hook stops Claude from reading .env files?

A PreToolUse hook matching Read and Grep (add Glob and Bash in practice), exiting 2 when the target is a .env file. Pair it with Read(./.env) deny rules in your permissions, because @ file mentions skip tool calls and therefore skip hooks.

What hook events does Claude Code support?

33 as of October 2026, including PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, SubagentStop, Notification, PreCompact and SessionEnd. See the full table above for when each fires, what its matcher matches, and whether it can block.

What is a Claude Code Stop hook?

A Stop hook is a hook that fires on the Stop event — when Claude finishes responding. If your Stop hook exits 2, Claude is forced to keep working, which is how you enforce "don't stop until tests pass" or "don't stop until the types compile." The critical detail: the hook's JSON input includes a stop_hook_active field, and your script must exit 0 when it is true, otherwise the Stop hook loops forever. See example 6 above for a working script.

What Comes Next

Hooks are how you take a single-developer tool and turn it into a team-grade platform. Once your team has a .claude/settings.json with the right guardrails — formatters running, audit logs flowing, dangerous commands gated behind approvals — Claude Code stops being "the thing one engineer uses" and starts being infrastructure.

The next layer is connecting Claude to the rest of your stack: GitHub, Slack, your database, your CRM. That is what MCP servers do. Hooks plus MCP plus a thoughtful Claude Code setup is the full stack — deterministic guardrails, rich integrations, and a repeatable team workflow.


Want hooks, MCP servers, and a custom Claude Code setup designed for your team? At Sandlabs, we design and ship Claude-native systems in 2–6 weeks with fixed pricing. Talk to us.

More articles

Claude Academy (2026): Every Free Course and What to Take First

Claude Academy (formerly Anthropic Academy) has 28 free courses with completion badges. The full catalogue, the right starting course for your role, badges vs paid certifications, and how to use it for a team.

Read more

Claude Pricing (October 2026): Every Plan and Tier in USD + AUD

Claude Pro is US$20/mo (A$34 in Australia), Max US$100, the new Ultra US$200, Team from US$20/seat. Every plan, API rate, discount and GST rule, re-checked 31 Oct 2026.

Read more

Let's build something great together.

Melbourne, Australia — serving founders worldwide. [email protected]