Claude Code Hooks: Complete Guide with 20+ Ready-to-Use Examples (2026)

Learn Claude Code hooks from zero to production. 20+ copy-paste configurations for auto-formatting, security, notifications, and advanced prompt hooks. The complete 2026 reference.

Lukas, Founder
Lukas, Founder
17 min read

Claude Code hooks are shell commands, LLM prompts, or subagents that execute automatically at specific points in Claude Code's lifecycle. They let you enforce formatting on every file write, block dangerous commands before they run, inject context after compaction, and automate any repetitive workflow — without relying on the AI to remember.

Key Takeaway: Hooks give you deterministic control over an probabilistic system. Instead of hoping Claude Code remembers to lint your files, a PostToolUse hook runs Prettier automatically, every single time.

This guide covers everything: setup, all 12 hook events, 20+ ready-to-use configurations, advanced prompt/agent hooks, and troubleshooting. Whether you've never configured a hook or want to build production-grade automation, start here.

New to Claude Code? Start with Claude Code best practices first — hooks build on top of a solid CLAUDE.md setup.

What you'll learn:

  1. What hooks are and why they matter
  2. Your first hook in 2 minutes
  3. All 12 hook events explained
  4. 20+ ready-to-use configurations
  5. Advanced: prompt hooks and agent hooks
  6. When to use hooks vs. CLAUDE.md vs. MCP
  7. Troubleshooting common issues

What Are Claude Code Hooks?

Every Claude Code session follows a lifecycle: session starts, user prompts, tools execute, agent responds. Hooks let you inject your own code at any point in that cycle.

Think of them like Git hooks (pre-commit, post-merge), but for AI-assisted development. Git hooks run before/after Git operations. Claude Code hooks run before/after any Claude Code action — file writes, bash commands, even agent decisions.

Why not just put instructions in CLAUDE.md?

CLAUDE.md is a suggestion. Claude Code usually follows it, but it's not guaranteed. Hooks are deterministic — they always run. Use CLAUDE.md for guidelines ("prefer Bun over npm"). Use hooks for rules that must never be broken ("always format with Prettier", "never touch .env files").

Three types of hooks:

TypeWhat it doesBest for
CommandRuns a shell scriptFormatting, linting, logging, security checks
PromptAsks an LLM a yes/no questionComplex decisions that shell scripts can't handle
AgentSpawns a multi-turn subagentVerification tasks requiring file reads + command execution

Your First Hook: Auto-Format Every File Edit

Let's set up a hook that runs Prettier on every file Claude Code writes or edits. Two minutes, zero complexity.

Step 1: Open your project's hook settings:

# Project-level (shared with team via git)
.claude/settings.json

# Or user-level (applies to all projects)
~/.claude/settings.json

Step 2: Add this configuration:

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

Step 3: That's it. Every file Claude Code writes or edits now gets auto-formatted.

What's happening:

  1. Claude Code writes a file (triggers PostToolUse for Write or Edit tools)
  2. Hook receives JSON with the file path via stdin
  3. jq extracts the path, Prettier formats the file
  4. exit 0 ensures the hook never blocks Claude Code (even if Prettier fails on a non-supported file)

Verify it works: Ask Claude Code to create any file. Check git diff — you'll see Prettier's formatting applied automatically.

How Hooks Work: Input, Output, Exit Codes

Every hook receives JSON context on stdin and communicates through exit codes and stdout.

Input: JSON describing the current event. For a PreToolUse Bash hook:

{
  "session_id": "abc123",
  "cwd": "/your/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf node_modules"
  }
}

Exit codes determine what happens:

Exit codeMeaningEffect
0SuccessAction proceeds. Stdout parsed as JSON or added as context.
2BlockAction is blocked. Stderr shown as feedback.
OtherNon-blocking errorAction proceeds. Stderr shown in verbose mode.

JSON output (optional, on stdout) controls behavior:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "This command is blocked by project policy"
  }
}

All 12 Hook Events

Claude Code has 12 lifecycle events where hooks can fire. Here's the complete reference, grouped by category.

Session Lifecycle

EventWhenCan block?Matcher values
SessionStartSession begins or resumesNostartup, resume, compact, clear
PreCompactBefore context compactionNomanual, auto
SessionEndSession terminatesNoclear, logout, other

SessionStart is particularly useful for injecting dynamic context:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: Use Bun, not npm. Current sprint: auth refactor. Run bun test before committing.'"
          }
        ]
      }
    ]
  }
}

This injects reminders after every compaction — so Claude Code never forgets your project context, even in long sessions.

Pro tip: Use SessionStart with the startup matcher to set environment variables:

#!/bin/bash
# .claude/hooks/set-env.sh
if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo "export NODE_ENV=development" >> "$CLAUDE_ENV_FILE"
  echo "export DEBUG=true" >> "$CLAUDE_ENV_FILE"
fi
exit 0

Tool Lifecycle

EventWhenCan block?Matcher values
PreToolUseBefore tool executesYesTool name: Bash, Edit, Write, Read, Glob, Grep, WebFetch, mcp__*
PostToolUseAfter tool succeedsNoSame as PreToolUse
PostToolUseFailureAfter tool failsNoSame as PreToolUse
PermissionRequestPermission dialog appearsYesSame as PreToolUse

PreToolUse is the workhorse. Use it to block, modify, or auto-approve tool calls:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Auto-approved by project policy"
  }
}

Three permission decisions:

  • "allow" — bypass permission system, auto-approve
  • "deny" — block the tool call
  • "ask" — show the normal permission prompt

PostToolUse is great for reactions: formatting, logging, running tests. It can't block (the action already happened), but it can give Claude Code feedback that influences its next steps.

Agent Lifecycle

EventWhenCan block?Matcher values
SubagentStartSubagent spawnsNoAgent type: Bash, Explore, Plan
SubagentStopSubagent finishesYesSame as SubagentStart
StopMain agent finishesYesNone (always fires)

Stop hooks are powerful — they can prevent Claude Code from finishing until conditions are met:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/verify-tests.sh"
          }
        ]
      }
    ]
  }
}

Critical: Always check stop_hook_active to prevent infinite loops:

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # Let Claude stop — we already verified
fi
# Your verification logic here

User Interaction

EventWhenCan block?Matcher values
UserPromptSubmitUser submits a promptYesNone (always fires)
NotificationClaude sends notificationNopermission_prompt, idle_prompt

UserPromptSubmit can validate, transform, or block user prompts before Claude Code processes them.

Ready-to-Use Hook Configurations

Copy-paste these into your .claude/settings.json. Each one solves a real workflow problem.

Security and Protection

1. Block destructive commands

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'CMD=$(jq -r \".tool_input.command\" <<< \"$(cat)\"); for p in \"rm -rf /\" \"rm -rf ~\" \"drop table\" \"DROP TABLE\" \"truncate\" \"TRUNCATE\" \"--force\" \"push.*--force\"; do if echo \"$CMD\" | grep -qiE \"$p\"; then echo \"Blocked: pattern \\\"$p\\\" detected\" >&2; exit 2; fi; done; exit 0'"
          }
        ]
      }
    ]
  }
}

2. Protect sensitive files from writes

Save as .claude/hooks/protect-files.sh:

#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED=(".env" ".env.local" "secrets/" ".git/" "package-lock.json" "pnpm-lock.yaml")

for pattern in "${PROTECTED[@]}"; do
  if [[ "$FILE" == *"$pattern"* ]]; then
    echo "Protected file: $pattern" >&2
    exit 2
  fi
done
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

3. Audit log all bash commands

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'jq -r \".tool_input.command\" <<< \"$(cat)\" | while read cmd; do echo \"$(date +%Y-%m-%dT%H:%M:%S) $cmd\" >> \"$CLAUDE_PROJECT_DIR\"/.claude/command-audit.log; done; exit 0'"
          }
        ]
      }
    ]
  }
}

Code Quality

4. Auto-format with Prettier (already shown above)

5. Auto-lint with ESLint and fix

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path\" <<< \"$(cat)\"); if [[ \"$FILE\" == *.ts || \"$FILE\" == *.tsx || \"$FILE\" == *.js || \"$FILE\" == *.jsx ]]; then npx eslint --fix \"$FILE\" 2>/dev/null; fi; exit 0'"
          }
        ]
      }
    ]
  }
}

6. Run type checking after TypeScript changes

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path\" <<< \"$(cat)\"); if [[ \"$FILE\" == *.ts || \"$FILE\" == *.tsx ]]; then npx tsc --noEmit 2>&1 | head -20; fi; exit 0'",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

7. Enforce test coverage — prevent stopping without tests

Save as .claude/hooks/verify-tests.sh:

#!/bin/bash
INPUT=$(cat)

# Prevent infinite loop
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi

# Run tests
if ! npm test --silent 2>/dev/null; then
  echo "Tests are failing. Fix them before finishing." >&2
  exit 2
fi

exit 0
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify-tests.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Workflow Automation

8. Inject context after compaction

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'echo \"Post-compaction context: Use Bun (not npm). Run bun test before committing. Current branch: $(git -C \"$CLAUDE_PROJECT_DIR\" branch --show-current 2>/dev/null || echo unknown). Last commit: $(git -C \"$CLAUDE_PROJECT_DIR\" log --oneline -1 2>/dev/null || echo none).\"'"
          }
        ]
      }
    ]
  }
}

9. Set environment variables on session start

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'if [ -n \"$CLAUDE_ENV_FILE\" ]; then echo \"export NODE_ENV=development\" >> \"$CLAUDE_ENV_FILE\"; echo \"export NEXT_TELEMETRY_DISABLED=1\" >> \"$CLAUDE_ENV_FILE\"; fi; exit 0'"
          }
        ]
      }
    ]
  }
}

10. Auto-run tests when test files change

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path\" <<< \"$(cat)\"); if [[ \"$FILE\" == *.test.* || \"$FILE\" == *.spec.* ]]; then npx vitest run \"$FILE\" 2>&1 | tail -5; fi; exit 0'",
            "timeout": 30,
            "async": true
          }
        ]
      }
    ]
  }
}

11. Notify on branch switch (inject branch context)

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'BRANCH=$(git -C \"$CLAUDE_PROJECT_DIR\" branch --show-current 2>/dev/null); echo \"Current branch: $BRANCH. Recent commits: $(git -C \"$CLAUDE_PROJECT_DIR\" log --oneline -3 2>/dev/null)\"'"
          }
        ]
      }
    ]
  }
}

Notifications

12. Desktop notification on permission request (macOS)

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

13. Desktop notification on task complete (macOS)

{
  "hooks": {
    "Notification": [
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Task complete — ready for next instruction\" with title \"Claude Code\" sound name \"Glass\"'"
          }
        ]
      }
    ]
  }
}

14. Linux notifications

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt|idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Needs your attention' --urgency=normal"
          }
        ]
      }
    ]
  }
}

MCP Tool Hooks

15. Log all MCP operations

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "mcp__.*",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'INPUT=$(cat); TOOL=$(echo \"$INPUT\" | jq -r \".tool_name\"); echo \"$(date +%H:%M:%S) $TOOL\" >> \"$CLAUDE_PROJECT_DIR\"/.claude/mcp-audit.log; exit 0'"
          }
        ]
      }
    ]
  }
}

16. Rate-limit specific MCP tools

#!/bin/bash
# .claude/hooks/rate-limit-mcp.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
LOGFILE="$CLAUDE_PROJECT_DIR/.claude/mcp-rate.log"

# Count calls in last 60 seconds
RECENT=$(grep -c "$TOOL" "$LOGFILE" 2>/dev/null || echo 0)
echo "$(date +%s) $TOOL" >> "$LOGFILE"

if [ "$RECENT" -gt 10 ]; then
  echo "Rate limit: $TOOL called $RECENT times in the last minute" >&2
  exit 2
fi
exit 0

Custom Permission Policies

17. Auto-approve WebSearch and WebFetch

Tired of approving every domain one by one? This hook eliminates the nagging:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "WebFetch|WebSearch",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"decision\":\"allow\"}'",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

WebSearch and WebFetch are read-only — auto-approving them is safe. See the full writeup for why this works and how to clean up your existing domain allowlist.

18. Auto-approve safe read operations

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Glob|Grep",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'echo \"{\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PreToolUse\\\",\\\"permissionDecision\\\":\\\"allow\\\"}}\"'"
          }
        ]
      }
    ]
  }
}

19. Deny web access in offline mode

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "WebFetch|WebSearch",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'echo \"Web access disabled by project policy\" >&2; exit 2'"
          }
        ]
      }
    ]
  }
}

Advanced: Prompt Hooks and Agent Hooks

Most articles only cover command hooks (shell scripts). But Claude Code supports two more powerful types that almost nobody talks about.

Prompt Hooks: LLM-Powered Decisions

When your validation logic is too complex for a bash script, let an LLM decide:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Review the conversation. Did the user's request get fully completed? Check: all files created, tests passing, no TODO comments left. Respond with {\"ok\": true} if done, or {\"ok\": false, \"reason\": \"what remains\"} if not.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

The LLM must respond with {"ok": true} or {"ok": false, "reason": "..."}. If ok is false, Claude Code continues working.

When to use prompt hooks:

  • Code review quality gates ("does this follow our patterns?")
  • Semantic validation ("is this commit message descriptive enough?")
  • Complex decision-making that can't be reduced to grep/regex

Agent Hooks: Multi-Turn Verification

Agent hooks can read files, run commands, and make multi-step decisions:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify the work is complete: 1) Run the test suite. 2) Check for any TypeScript errors. 3) Verify no console.log statements were left in production code. Report your findings. $ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Agent hooks can use up to 50 tool turns — they can read files, grep for patterns, run bash commands, and make complex assessments.

When to use agent hooks:

  • End-of-task verification (tests pass, types check, no debug code)
  • Multi-file consistency checks
  • Complex pre-deployment validations

Combining All Three Types

A production-grade setup might layer all three:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null; exit 0"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify-tests.sh"
          },
          {
            "type": "prompt",
            "prompt": "Check if the implementation matches what the user asked for. Are there any edge cases missed? Respond with {\"ok\": true} or {\"ok\": false, \"reason\": \"...\"}.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Command hooks handle the deterministic stuff (formatting). Prompt hooks handle the semantic stuff (does this match the request?). Each runs in sequence — if the command hook blocks (exit 2), the prompt hook doesn't run.

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

Claude Code has several extension points. Here's when to use each one:

NeedUseWhy
"Always format files on save"Hook (PostToolUse)Must happen every time, no exceptions
"Prefer Bun over npm"CLAUDE.mdA preference, not a hard rule
"Never modify .env files"Hook (PreToolUse)Hard block, not a suggestion
"Our API routes follow this pattern".claude/rules/Contextual guidance
"Run /deploy to ship"Custom commandReusable workflow
"Access our Jira board"MCP serverExternal service integration
"Log every command to audit file"Hook (PostToolUse)Side effect, transparent to Claude
"Verify tests pass before stopping"Hook (Stop)Enforcement gate

Rule of thumb: If it's a suggestion, use CLAUDE.md. If it's a requirement, use hooks. If it's an external service, use MCP. If it's a reusable workflow, use custom commands.

Troubleshooting

"My hook isn't firing"

  1. Check configuration loaded: Run /hooks in Claude Code to see active hooks
  2. Matcher is case-sensitive: bash won't match Bash. Tool names are PascalCase.
  3. Wrong event: PostToolUse fires after success only. For failures, use PostToolUseFailure.
  4. File permissions: Ensure your script is executable: chmod +x .claude/hooks/your-script.sh

"JSON validation failed" error

Your shell profile (.bashrc, .zshrc) is printing output that corrupts the JSON. Fix:

# In your .bashrc/.zshrc — wrap interactive-only output
if [[ $- == *i* ]]; then
  echo "Welcome!"  # Only runs in interactive shells
fi

"Hook blocks everything" (infinite Stop loop)

Your Stop hook keeps blocking Claude Code from finishing. Always check stop_hook_active:

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # Allow stop — we already had our chance
fi
# Your logic here

"jq: command not found"

Install jq — it's required for parsing JSON input:

# macOS
brew install jq

# Ubuntu/Debian
sudo apt-get install jq

# Or use Python as fallback
python3 -c "import sys,json; print(json.load(sys.stdin)['tool_input']['file_path'])"

Testing hooks manually

Don't guess — test directly:

# Simulate a Bash PreToolUse event
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | ./.claude/hooks/block-dangerous.sh
echo "Exit code: $?"

# Expected: exit code 2 (blocked)

Viewing hook execution in real-time

# Start Claude Code with debug output
claude --debug

# Or toggle verbose mode during a session
# Press Ctrl+O

Frequently Asked Questions

What are Claude Code hooks?

Hooks are user-defined shell commands, LLM prompts, or subagents that run automatically at specific points in Claude Code's lifecycle. They provide deterministic control — ensuring actions like formatting, linting, or security checks always happen, rather than relying on the LLM to remember.

Where do I configure Claude Code hooks?

In JSON settings files at three levels: ~/.claude/settings.json (all projects), .claude/settings.json (single project, shareable via git), and .claude/settings.local.json (project-specific, gitignored). Use the /hooks menu in Claude Code to view and manage your hooks.

Can hooks block dangerous commands?

Yes. PreToolUse hooks can block any tool call by returning exit code 2 or outputting a JSON decision with permissionDecision set to "deny". This is commonly used to prevent destructive commands like rm -rf, DROP TABLE, or force pushes.

What's the difference between command, prompt, and agent hooks?

Command hooks run shell scripts — best for deterministic tasks like formatting or logging. Prompt hooks use an LLM for yes/no decisions when logic is too complex for shell scripts. Agent hooks spawn a multi-turn subagent that can read files, run commands, and verify complex conditions.

Do hooks slow down Claude Code?

Command hooks add minimal overhead — typically milliseconds. Prompt and agent hooks are slower (they call the LLM), but you can set timeouts. Use async: true for long-running hooks that don't need to block the current action.

Can I use hooks with MCP tools?

Yes. Use regex matchers like mcp__github__.* to target specific MCP server tools, or mcp__.* to match all MCP tools.

Pre-built hooks, commands, and workflows — ready to go

AI Org kits come with production-grade hook configurations, custom commands, and domain-specific rules. Install in seconds.

See all kits

Frequently Asked Questions

What are Claude Code hooks?
Hooks are user-defined shell commands, LLM prompts, or subagents that run automatically at specific points in Claude Code's lifecycle. They provide deterministic control — ensuring actions like formatting, linting, or security checks always happen, rather than relying on the LLM to remember.
Where do I configure Claude Code hooks?
In JSON settings files at three levels: ~/.claude/settings.json (all projects), .claude/settings.json (single project, shareable via git), and .claude/settings.local.json (project-specific, gitignored). Use the /hooks menu in Claude Code to view and manage your hooks.
Can hooks block Claude Code from running dangerous commands?
Yes. PreToolUse hooks can block any tool call by returning exit code 2 or outputting a JSON decision with permissionDecision set to deny. This is commonly used to prevent destructive commands like rm -rf, DROP TABLE, or force pushes.
What is the difference between command hooks, prompt hooks, and agent hooks?
Command hooks run shell scripts and are best for deterministic tasks like formatting or logging. Prompt hooks use an LLM to make yes/no decisions when logic is too complex for shell scripts. Agent hooks spawn a multi-turn subagent that can read files, run commands, and verify complex conditions before allowing Claude to continue.
How do I debug Claude Code hooks that aren't working?
Three approaches: 1) Run claude --debug to see hook execution details. 2) Use /hooks in Claude Code to verify your configuration is loaded. 3) Test hooks manually by piping JSON to your script: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./your-hook.sh && echo $?
Do hooks slow down Claude Code?
Command hooks add minimal overhead — typically milliseconds for simple scripts. Prompt hooks and agent hooks are slower because they call the LLM, but you can set timeouts to cap execution time. Use async: true for long-running hooks that don't need to block the action.
Can I use hooks with MCP tools?
Yes. Use regex matchers to target MCP tools. For example, matcher mcp__github__.* matches all GitHub MCP tools, and mcp__memory__.* matches all memory MCP tools. This works for PreToolUse, PostToolUse, and PermissionRequest events.