Claude Code Hooks (2026): 6 Production Hooks + Common Gotchas | Setup Guide

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, and six hooks we run on real consulting projects (including a Stop hook). Current as of June 2026.

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 exposes more than 25 hook events. 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.

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. 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.

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.

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.

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 Certification 2026: All Four Anthropic Exams, What They Cost, and Whether Your Team Needs One

Anthropic now runs four proctored Claude certifications through Pearson VUE. What CCAO-F, CCDV-F, CCAR-F and CCAR-P actually test, what they cost in AUD, how to study for free, and the honest answer on whether a certificate changes anything for your business.

Read more

Does Anthropic Charge GST in Australia? Claude Invoices, ABNs and Your BAS

Anthropic adds 10% GST to Australian Claude subscriptions by default - and if you do not add your ABN, you probably cannot claim it back. How the imported-services rules work, the two-minute fix, and what to do about invoices you have already paid.

Read more

Let's build something great together.

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