ai-machine-learning

Claude Code Hooks: The Complete Developer Guide with Production-Ready Examples

Written by Mert Batur
Apr 5, 2026
16 read
Claude Code Hooks: The Complete Developer Guide with Production-Ready Examples

Claude Code Hooks: The Complete Developer Guide with Production-Ready Examples

Claude Code is excellent at writing code, but it's still a probabilistic system. You can ask it to run Prettier after every file edit. You can put that instruction in your CLAUDE.md. And sometimes, it'll just... forget. Claude Code hooks solve this by giving you deterministic, guaranteed control over what happens before, during, and after every action Claude takes.

I've been configuring hooks across dozens of projects over the past few months, and they've quietly become the most important part of my Claude Code setup. This guide covers everything from the basics to a production-ready starter kit you can drop into any project today. If you've used Claude Code alongside tools like Cursor or Copilot, you already know the value of customization, hooks take that a step further.

What Are Claude Code Hooks (and Why Should You Care)?

Claude Code hooks are user-defined shell commands, HTTP endpoints, or LLM prompts that execute automatically at specific points in Claude Code's lifecycle. According to Anthropic's official documentation, unlike prompt instructions that Claude might ignore, hooks fire deterministically every time, giving you guaranteed control over formatting, security, notifications, and workflow automation.

The Probabilistic Problem

Here's the thing about CLAUDE.md instructions: they're suggestions, not contracts. You can write "always run npx prettier --write after editing TypeScript files" in your project context, and Claude will follow it most of the time. But "most of the time" isn't good enough when you're enforcing code formatting across a team, or blocking pushes to production, or logging every shell command for a security audit.

This is the core tension in any AI coding tool. Claude is a language model, it operates on probabilities. Your context engineering can nudge behavior, but it can't guarantee it.

How Hooks Solve This

Hooks bypass the LLM entirely. They're shell scripts, HTTP calls, or AI evaluations that fire at specific lifecycle events, before a tool runs (PreToolUse), after it completes (PostToolUse), when a notification appears, when a session starts, or when Claude stops. Think of them like Git hooks, but for your AI coding assistant.

Four hook types exist: command (shell scripts), HTTP (webhook POST requests), prompt (single-turn Claude yes/no evaluations), and agent (spawns a subagent with tool access). We'll break each one down later, command hooks handle roughly 90% of what you'll need.

How Claude Code Hooks Work: The Lifecycle Flow

Claude Code hooks execute in a defined lifecycle: an event fires (e.g., PreToolUse), the matcher checks if the hook applies, the hook script runs and receives JSON on stdin, and the exit code determines what happens next. Exit code 0 means proceed, exit code 2 means block the action. This flow is the same regardless of which hook type you're using.

Event -> Matcher -> Hook -> Exit Code (The 4-Step Flow)

Here's how every hook execution works:

text
1. EVENT FIRES          e.g., PreToolUse(Write)
       |
2. MATCHER CHECKS       Does "Write" match the hook's matcher pattern?
       |
3. HOOK EXECUTES        Shell script runs, receives JSON via stdin
       |
4. EXIT CODE DECIDES    0 = proceed | 2 = block | other = error

The JSON that arrives on stdin contains everything about the event: the tool_name, tool_input (file path, content, command), and session metadata. Your script reads this JSON, does whatever logic it needs, and exits with the appropriate code.

For PreToolUse hooks, exit code 2 is the powerful one, it blocks the action entirely and sends your stdout message back to Claude as feedback. Claude sees your message and can adjust its approach.

Configuration Scopes: User, Project, and Local

Hooks live in settings.json at three levels:

ScopeFileCommitted to Git?Use Case
User~/.claude/settings.jsonNoPersonal defaults (notifications, formatting preferences)
Project.claude/settings.jsonYesTeam-shared hooks (file protection, test runners, linting)
Local.claude/settings.local.jsonNo (gitignored)Personal overrides for this project

Project settings are the most useful for teams. Drop your hooks into .claude/settings.json, commit it, and every developer on the team gets the same guardrails automatically.

The if Field: Fine-Grained Filtering

Since Claude Code v2.1.85, hooks support an if field that lets you filter by tool arguments, not just tool names. As documented in the Anthropic hooks reference, this means you can write a hook that only triggers on Bash commands matching git push, instead of firing on every single Bash invocation.

json
{
  "matcher": "Bash",
  "if": "tool_input.command matches 'git push'",
  "hooks": [{ "type": "command", "command": "./scripts/check-branch.sh" }]
}

This was a major improvement. Before if, you'd either match too broadly (every Bash command) or do the filtering inside your script (messy).

All Claude Code Hook Events: Quick Reference Table

Claude Code provides over 20 hook events across its lifecycle, as documented in the official hooks reference and the Claude Code changelog. The most commonly used are PreToolUse, PostToolUse, Notification, and Stop, but newer events like ConfigChange and FileChanged open up advanced automation patterns.

Here's the complete reference:

EventWhen It FiresCan Block?Common Use Case
PreToolUseBefore a tool executesYes (exit 2)Block dangerous commands, protect files
PostToolUseAfter a tool completesNoAuto-format, run tests, log actions
NotificationWhen Claude sends a notificationNoDesktop alerts, Slack messages
StopWhen Claude finishes a responseNoCleanup, summary generation
SessionStartAt session initializationNoInject context, set environment
UserPromptSubmitWhen user submits a promptYes (exit 2)Input validation, content filtering
PreCompactBefore context compactionNoSave state before memory is trimmed
PostCompactAfter context compactionNoRe-inject critical context
ConfigChangeWhen settings changeNoHot-reload environment variables
FileChangedWhen a watched file changesNoTrigger rebuilds, invalidate caches
TaskCreatedWhen a new task is spawnedNoTask tracking, resource allocation
PermissionDeniedWhen a permission check failsNoAudit logging, alert on blocked actions
WorktreeCreateWhen a new Git worktree is createdNoInitialize worktree-specific settings
SubagentStartWhen a subagent spawnsNoMonitor subagent activity
SubagentStopWhen a subagent completesNoValidate subagent output

Pro tip: You'll use PreToolUse and PostToolUse for 80% of your hooks. SessionStart is the next most useful, it's perfect for injecting project context that Claude needs at the start of every session.

The 4 Claude Code Hook Types Explained

Claude Code supports four hook handler types: command hooks run shell scripts, HTTP hooks POST to URLs, prompt hooks ask Claude a yes/no question, and agent hooks spawn a subagent with tool access. In our experience, command hooks handle 90% of use cases. Use HTTP for external integrations, prompt and agent hooks for nuanced decisions that need AI judgment.

TypeSpeedComplexityBest ForExample
CommandFastLowFormatting, blocking, loggingRun Prettier after file edit
HTTPMediumMediumExternal services, webhooksPOST to Slack on completion
PromptSlowMediumSubjective decisions"Is this code safe to run?"
AgentSlowestHighComplex file-aware verificationCheck if new code follows project patterns

Command Hooks (The Workhorse)

Command hooks run a shell command and use the exit code to determine the outcome. They receive the event's JSON data on stdin.

json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "jq -r '.tool_input.command' | grep -q 'rm -rf /' && exit 2 || exit 0"
      }]
    }]
  }
}

This is what you'll use for formatting, file protection, notifications, and most automation. Fast, simple, and predictable.

HTTP Hooks (External Integrations)

HTTP hooks send a POST request to a URL with the event JSON as the body. The response status code determines the outcome (200 = proceed, 403 = block).

json
{
  "hooks": {
    "Stop": [{
      "matcher": "",
      "hooks": [{
        "type": "http",
        "url": "https://your-api.com/claude-webhook"
      }]
    }]
  }
}

Great for sending events to Slack, Discord, PagerDuty, or a custom dashboard. You could also use this to query an external policy engine before allowing a tool execution.

Prompt Hooks (AI-Powered Decisions)

Prompt hooks pass the event data to Claude itself for a single-turn yes/no evaluation. Claude returns a JSON response with "decision": "allow" or "decision": "block" plus reasoning.

json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "prompt",
        "prompt": "Is this bash command safe to run in a production environment? Consider: does it modify system files, delete data, or access sensitive credentials?"
      }]
    }]
  }
}

Use these sparingly. They add latency (a full LLM call per hook execution) and cost. But for genuinely subjective safety checks, like "does this database migration look destructive?", they're hard to beat. If you're curious about switching Claude Code models, the model used for prompt hooks follows your current session model.

Agent Hooks (Tool-Assisted Verification)

Agent hooks spawn a subagent with access to Read, Grep, and Glob tools. The subagent can inspect files before making its decision.

json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "agent",
        "prompt": "Check if the file being written follows the project's naming conventions and import patterns. Read .claude/CONVENTIONS.md for the rules."
      }]
    }]
  }
}

This is the most powerful hook type, but also the slowest. Reserve it for high-stakes checks where you need file context to make a good decision.

7 Production-Ready Claude Code Hook Examples (Copy-Paste Ready)

The most useful Claude Code hooks include auto-formatting with Prettier or Black after file edits, blocking writes to protected files, sending desktop notifications on task completion, injecting project context at session start, running tests after code changes, enforcing branch protection, and auditing all tool usage. I've been running variations of these across every project for the past three months.

Each example below is a complete settings.json snippet you can drop into your .claude/settings.json. Community collections like awesome-claude-code have even more patterns.

1. Auto-Format on Save

json
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "FILE=$(jq -r '.tool_input.file_path // .tool_input.file' /dev/stdin); case \"$FILE\" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write \"$FILE\" 2>/dev/null;; *.py) black \"$FILE\" 2>/dev/null;; esac; exit 0"
      }]
    }]
  }
}

This fires after every Write or Edit, extracts the file path from stdin JSON, and runs the appropriate formatter. The exit 0 at the end ensures the hook never blocks, formatting failures shouldn't stop Claude.

Pro tip: Add *.go with gofmt and *.rs with rustfmt if you work across languages.

2. Block Writes to Protected Files

json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "if": "tool_input.file_path matches '(\\.env|\\.env\\.local|package-lock\\.json|yarn\\.lock|pnpm-lock\\.yaml)'",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"BLOCKED: This file is protected. Edit it manually.\"}' && exit 2"
      }]
    }]
  }
}

Exit code 2 blocks the action and sends the JSON message back to Claude. Claude sees the feedback and adjusts, usually it'll tell you it wanted to modify the file and ask you to do it manually. The if field keeps this from firing on every single Write.

3. Desktop Notification on Completion

json
{
  "hooks": {
    "Notification": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "MSG=$(jq -r '.message // \"Claude Code task finished\"' /dev/stdin); if [ \"$(uname)\" = 'Darwin' ]; then osascript -e \"display notification \\\"$MSG\\\" with title \\\"Claude Code\\\"\"; else notify-send 'Claude Code' \"$MSG\"; fi; exit 0"
      }]
    }]
  }
}

Works on macOS (osascript) and Linux (notify-send). The empty matcher means it fires on all notifications. This is genuinely useful when you kick off a long task and switch to another window.

4. Context Injection at Session Start

json
{
  "hooks": {
    "SessionStart": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"Project: '\"$(basename $(pwd))\"' | Branch: '\"$(git branch --show-current 2>/dev/null || echo none)\"' | Last commit: '\"$(git log --oneline -1 2>/dev/null || echo none)\"'\"}'; exit 0"
      }]
    }]
  }
}

This injects the current project name, Git branch, and last commit into every session. Claude receives this context automatically, no need to tell it which branch you're on.

5. Auto-Run Tests After Code Changes

json
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "if": "tool_input.file_path matches '\\.(ts|tsx|js|jsx|py)$'",
      "hooks": [{
        "type": "command",
        "command": "FILE=$(jq -r '.tool_input.file_path' /dev/stdin); TEST_FILE=$(echo \"$FILE\" | sed 's/\\.[^.]*$/.test&/'); if [ -f \"$TEST_FILE\" ]; then npx jest \"$TEST_FILE\" --no-coverage 2>&1 | tail -5; fi; exit 0",
        "timeout": 30000
      }]
    }]
  }
}

If a matching test file exists, it runs automatically after Claude edits the source. The tail -5 keeps the output concise, and the timeout prevents runaway test suites. This pairs well with an AI-powered code review workflow.

6. Branch Protection Enforcement (Advanced)

json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "if": "tool_input.command matches 'git push.*(main|master|production)'",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"BLOCKED: Direct push to protected branch. Use a feature branch and open a PR.\"}' && exit 2"
      }]
    }]
  }
}

This blocks any git push that targets main, master, or production branches. Claude gets the feedback and will suggest creating a feature branch instead.

7. Security Audit Logging (Advanced)

json
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "INPUT=$(cat /dev/stdin); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command'); echo \"[$(date -u +%Y-%m-%dT%H:%M:%SZ)] BASH: $CMD\" >> .claude/audit.log; exit 0"
      }]
    }]
  }
}

Logs every Bash command Claude executes to an audit file with a UTC timestamp. Invaluable for security reviews and understanding what Claude actually did during a session. Keep .claude/audit.log in your .gitignore.

Hooks vs MCP vs Skills vs CLAUDE.md: When to Use What

Use hooks for deterministic automation that must always run (formatting, blocking, notifications). Use MCP for giving Claude access to external tools and data. Use Skills for reusable prompt packages. Use CLAUDE.md for behavioral guidance and project context. Hooks are guaranteed; everything else is probabilistic. This is the single most important distinction, and I keep coming back to it when advising teams.

The Decision Matrix

MechanismDeterministic?When It RunsBest ForExample
HooksYesAutomatically on lifecycle eventsEnforcement, automation, notificationsAuto-format, block file writes
MCPNo (Claude decides)When Claude calls the MCP toolNew capabilities, external data accessQuery a database, search Notion
SkillsNo (user triggers)When user invokes a slash commandReusable instruction sets/review for code review workflow
CLAUDE.mdNo (guidance)Read at session startProject context, coding standards"Use Tailwind, write tests for all new code"

For a deep dive on MCP, check our MCP guide. If you're coming from Cursor, Cursor's rules system is roughly analogous to CLAUDE.md, but Cursor doesn't have anything like hooks.

When They Overlap (and How to Choose)

Here's the flowchart I use:

  • "Does this NEED to happen every single time, no exceptions?", Hook. Format code, block protected files, send notifications. Zero ambiguity.
  • "Does Claude need a new CAPABILITY it doesn't have?", MCP server. Access a database, call an API, search external docs.
  • "Do I want reusable INSTRUCTIONS for a specific workflow?", Skill (slash command). Code review templates, deployment checklists.
  • "Do I want to shape Claude's BEHAVIOR in this project?", CLAUDE.md. Coding standards, architecture decisions, preferred libraries.

Real examples that clarify the boundary:

  • "Always format with Prettier" = Hook (it must happen every time)
  • "Use Prettier for formatting" in CLAUDE.md = Guidance (Claude might forget)
  • "Search our company docs" = MCP (new capability)
  • "Follow our style guide when reviewing code" = Skill or CLAUDE.md

As described in Anthropic's plugins announcement, hooks are one piece of a broader plugin ecosystem that also includes MCP and Skills. They're designed to complement each other, not compete.

The Starter Kit: Drop-In Claude Code Hooks Config for Any Project

A starter hooks configuration for Claude Code should include auto-format on file edit, notification on task completion, file protection for sensitive files, session context injection, and a stop hook for cleanup. This is the exact config I drop into every new project, adapted for the stack, but the structure stays the same.

The Config

json
{
  "hooks": {
    "SessionStart": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"Project: '\"$(basename $(pwd))\"' | Branch: '\"$(git branch --show-current 2>/dev/null)\"' | Node: '\"$(node -v 2>/dev/null)\"'\"}'; exit 0"
      }]
    }],
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "if": "tool_input.file_path matches '(\\.env|\\.env\\..+|.*lock\\.json|.*lock\\.yaml)'",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"Protected file. Edit manually.\"}' && exit 2"
      }]
    }],
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "FILE=$(jq -r '.tool_input.file_path // .tool_input.file' /dev/stdin); case \"$FILE\" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write \"$FILE\" 2>/dev/null;; *.py) black \"$FILE\" 2>/dev/null;; *.go) gofmt -w \"$FILE\" 2>/dev/null;; esac; exit 0"
      }]
    }],
    "Notification": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "MSG=$(jq -r '.message // \"Done\"' /dev/stdin); osascript -e \"display notification \\\"$MSG\\\" with title \\\"Claude Code\\\"\" 2>/dev/null || notify-send 'Claude Code' \"$MSG\" 2>/dev/null; exit 0"
      }]
    }],
    "Stop": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "echo '[STOP] '\"$(date +%H:%M:%S)\"'' >> .claude/session.log; exit 0"
      }]
    }]
  }
}

How to Customize for Your Stack

StackFormat CommandTest CommandWatch Extensions
Node/TypeScriptnpx prettier --writenpx jest --no-coverage.ts, .tsx, .js, .jsx
Pythonblackpytest -x.py
Gogofmt -wgo test ./....go
Rustrustfmtcargo test.rs

Swap the format and test commands in the config above to match your stack. The structure stays identical.

Verifying Your Hooks Work

Three ways to confirm hooks are active:

  1. /hooks command, Type /hooks in Claude Code to see all registered hooks, their matchers, and their status.
  2. Transcript inspection, After a hook fires, check the session transcript. Hook executions appear with their output and exit code.
  3. Quick toggle, Add "disableAllHooks": true to your settings.json to temporarily disable all hooks without deleting the config. Remove it (or set to false) to re-enable.

CI/CD Integration: Claude Code Hooks in Headless Mode

Claude Code hooks work in headless mode (claude -p) with some differences: Notification hooks still fire but you should redirect to logging instead of desktop alerts. PreToolUse hooks with exit code 2 can pause headless sessions for human review. GitHub Actions uses anthropics/claude-code-action@v1 alongside hooks for automated workflows.

Headless Mode Behavior

Hook EventInteractive ModeHeadless Mode (-p)CI Recommendation
PreToolUse (exit 2)Blocks, shows messagePauses for --resumeUse for mandatory human approvals
PostToolUseRuns normallyRuns normallyKeep formatters and loggers
NotificationDesktop alertStill fires (no UI)Redirect to log file or Slack webhook
StopRuns cleanupRuns cleanupGood for CI artifact collection
SessionStartInjects contextInjects contextInject CI environment variables

The big surprise in headless mode: PreToolUse hooks that exit with code 2 don't just fail silently. They pause the session and let you resume with --resume, which gives you a human-in-the-loop pattern for CI pipelines.

GitHub Actions Integration

Here's a minimal GitHub Actions workflow that uses Claude Code with hooks. As documented in the official GitHub Actions guide:

yaml
- name: Run Claude Code
  uses: anthropics/claude-code-action@v1
  with:
    prompt: "Review this PR and suggest improvements"
    allowed_tools: "Read,Grep,Glob"
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

Your .claude/settings.json hooks travel with the repo, so they'll fire in CI exactly like they do locally. Just make sure any hooks that rely on desktop-specific tools (like osascript) have fallbacks or conditionals.

Team Hook Management

A pattern that works well for teams:

  • .claude/settings.json (committed), Team-shared hooks: file protection, formatters, branch protection. Everyone gets these.
  • .claude/settings.local.json (gitignored), Personal hooks: notification preferences, custom logging, experimental hooks.
  • ~/.claude/settings.json (user-global), Your defaults across all projects: notification style, personal formatting preferences.

This mirrors how .editorconfig (committed) and local IDE settings (personal) work. As noted by Angelo Lima's CI/CD guide, teams that standardize on shared hooks see fewer "works on my machine" issues with Claude Code.

Troubleshooting Claude Code Hooks and Common Mistakes

Common Claude Code hooks issues include hooks not firing (check matcher spelling and settings.json location), hooks running but not blocking (wrong exit code, use 2 not 1), infinite loops (Stop hook triggering itself), and slow startup (too many synchronous hooks). The most common mistake I see is exit code confusion, developers use exit 1 when they mean exit 2.

Hook Not Firing

Symptoms: You added a hook but nothing happens when the event occurs.

Fixes:

  • Matcher typo, Matchers are case-sensitive. "write" won't match the Write tool. Check exact tool names with /hooks.
  • Wrong settings file, Hooks in ~/.claude/settings.json won't appear in /hooks output for the project scope. Try .claude/settings.json in the project root.
  • JSON syntax error, A stray comma or missing bracket silently disables the entire hooks config. Run your settings.json through jq . to validate.
  • disableAllHooks: true, Check if someone (or a previous debug session) left this flag on.

Hook Runs but Doesn't Block

Symptoms: Your PreToolUse hook executes, but the action still proceeds.

Fixes:

  • Wrong exit code, Exit code 1 means "error" (hook failed), not "block." Use exit 2 to block an action. This trips up almost everyone, as noted in the official docs.
  • Missing stdout JSON, For blocking hooks, output a JSON message so Claude knows why the action was blocked: echo '{"message": "Blocked: reason"}'

Infinite Loops

Symptoms: Claude keeps retrying the same action, or your machine heats up suspiciously.

Fixes:

  • Stop hook triggering actions, If your Stop hook writes a file or runs a command that causes Claude to respond, you've created a loop. Stop hooks should only do passive things: log, notify, clean up.
  • PostToolUse hook causing edits, A PostToolUse hook that modifies a file triggers another PostToolUse event. Guard against this with specific matchers or the if field.

Performance Issues

Symptoms: Claude takes noticeably longer to start or execute tools.

Fixes:

  • Too many SessionStart hooks, Each one runs synchronously at startup. Keep these lightweight (under 1 second each).
  • Heavy scripts in hot paths, Hooks on PreToolUse and PostToolUse fire frequently. If your script does network requests or heavy computation, add a timeout field (milliseconds) and consider whether it should be an HTTP hook instead.
  • No caching, If you're checking the same thing repeatedly (like "is this a protected branch?"), cache the result in a temp file instead of running Git commands on every hook invocation.

Frequently Asked Questions

What are Claude Code hooks and how do they work?

Claude Code hooks are user-defined automation scripts that execute at specific lifecycle events during a Claude Code session. You configure them in settings.json with a matcher pattern and a handler (shell command, HTTP endpoint, prompt, or agent). When the matching event fires, the hook runs automatically and uses exit codes to control the outcome.

How do I configure hooks in Claude Code settings.json?

Add a "hooks" object to any of the three config locations: ~/.claude/settings.json (user-global), .claude/settings.json (project-shared), or .claude/settings.local.json (project-personal). Each event type maps to an array of hook definitions with matcher, optional if field, and a hooks array containing handler objects with type and command or url.

What is the difference between PreToolUse and PostToolUse hooks?

PreToolUse fires before a tool executes, giving you the power to block it with exit code 2. PostToolUse fires after execution completes, useful for formatting, testing, or logging. PreToolUse is for prevention and gating. PostToolUse is for validation and cleanup. Both receive the tool name and input as JSON on stdin.

Can Claude Code hooks block dangerous commands?

Yes. PreToolUse hooks with exit code 2 block any tool execution. You can protect sensitive files from being written, block shell commands matching dangerous patterns like rm -rf or git push main, and prevent access to production databases. The blocking message is sent back to Claude as feedback, so it can adjust its approach.

What hook events are available in Claude Code?

Claude Code provides 15+ events: PreToolUse and PostToolUse for tool execution, Notification for alerts, Stop for session end, SessionStart for initialization, UserPromptSubmit for input filtering, PreCompact and PostCompact for context management, and newer events like ConfigChange, FileChanged, TaskCreated, and PermissionDenied. See the full reference table in the hook events section above.

How do hooks differ from MCP tools and Skills?

Hooks are deterministic, they always fire on matching events regardless of what Claude decides. MCP tools extend Claude's capabilities (database access, API calls) but Claude chooses when to use them. Skills are reusable instruction packages invoked by slash commands. CLAUDE.md provides behavioral guidance. Use hooks when something must happen every time, MCP when Claude needs new abilities.

Do Claude Code hooks work in headless mode?

Yes, with caveats. Hooks fire normally in headless mode (claude -p), but desktop-specific hooks like macOS notifications need fallbacks. Importantly, PreToolUse hooks that exit with code 2 can pause headless sessions for human approval via --resume. This enables human-in-the-loop CI/CD pipelines where certain actions require manual sign-off.

How many hooks is too many? Do hooks slow down Claude Code?

There's no hard limit, but each synchronous hook adds latency. SessionStart hooks run at startup, so keep them fast (under 1 second each). PreToolUse and PostToolUse hooks fire on every matching tool call, heavy scripts here compound quickly. I'd recommend keeping total hooks under 10-15, using the if field to narrow scope, and adding timeout values to prevent runaway scripts.

Can I use hooks to auto-format code with Prettier or Black?

Yes, it's the most popular hook use case. Create a PostToolUse hook matching Write|Edit, extract the file path from stdin JSON, and run the appropriate formatter based on file extension. See example number one in the production examples section for a complete, copy-paste-ready config that handles TypeScript, JavaScript, and Python files.

Are Claude Code hooks safe? What are the security risks?

Hooks run with your full user permissions, there's no sandbox. A malicious hook could read your SSH keys, delete files, or exfiltrate data. Only use hooks from trusted sources, review any shared .claude/settings.json before accepting it into your project, and use .claude/settings.local.json for personal hooks that shouldn't be shared. For broader AI safety patterns, see our LLM guardrails guide.

Tags

claude code hooksclaude codedeveloper toolsAI automationworkflow automationsettings.jsonPreToolUsePostToolUse

Share this article

Start Your Project

Ready to build something extraordinary?

Let's turn your vision into reality. Our team is ready to help you create software that makes a difference.