DEV Community

Lukas Fryc
Lukas Fryc

Posted on • Originally published at aiorg.dev

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

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 a 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:

Type What it does Best for
Command Runs a shell script Formatting, linting, logging, security checks
Prompt Asks an LLM a yes/no question Complex decisions that shell scripts can't handle
Agent Spawns a multi-turn subagent Verification 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
Enter fullscreen mode Exit fullscreen mode

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"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Exit codes determine what happens:

Exit code Meaning Effect
0 Success Action proceeds. Stdout parsed as JSON or added as context.
2 Block Action is blocked. Stderr shown as feedback.
Other Non-blocking error Action 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

All 12 Hook Events

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

Session Lifecycle

Event When Can block? Matcher values
SessionStart Session begins or resumes No startup, resume, compact, clear
PreCompact Before context compaction No manual, auto
SessionEnd Session terminates No clear, 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.'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Tool Lifecycle

Event When Can block? Matcher values
PreToolUse Before tool executes Yes Tool name: Bash, Edit, Write, Read, Glob, Grep, WebFetch, mcp__*
PostToolUse After tool succeeds No Same as PreToolUse
PostToolUseFailure After tool fails No Same as PreToolUse
PermissionRequest Permission dialog appears Yes Same 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Event When Can block? Matcher values
SubagentStart Subagent spawns No Agent type: Bash, Explore, Plan
SubagentStop Subagent finishes Yes Same as SubagentStart
Stop Main agent finishes Yes None (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"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

User Interaction

Event When Can block? Matcher values
UserPromptSubmit User submits a prompt Yes None (always fires)
Notification Claude sends notification No permission_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'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify-tests.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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).\"'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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)\"'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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\"'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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\"'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

14. Linux notifications

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

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'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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\\\"}}\"'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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'"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

Need Use Why
"Always format files on save" Hook (PostToolUse) Must happen every time, no exceptions
"Prefer Bun over npm" CLAUDE.md A 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 command Reusable workflow
"Access our Jira board" MCP server External 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
Enter fullscreen mode Exit fullscreen mode

"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
Enter fullscreen mode Exit fullscreen mode

"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'])"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Viewing hook execution in real-time

# Start Claude Code with debug output
claude --debug

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

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.


I'm Lukas — I build AI-powered dev tools for solo founders. More on Claude Code workflows and AI-native development on my blog.

Top comments (0)