
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:
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 = errorThe 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:
| Scope | File | Committed to Git? | Use Case |
|---|---|---|---|
| User | ~/.claude/settings.json | No | Personal defaults (notifications, formatting preferences) |
| Project | .claude/settings.json | Yes | Team-shared hooks (file protection, test runners, linting) |
| Local | .claude/settings.local.json | No (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.
{
"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:
| Event | When It Fires | Can Block? | Common Use Case |
|---|---|---|---|
| PreToolUse | Before a tool executes | Yes (exit 2) | Block dangerous commands, protect files |
| PostToolUse | After a tool completes | No | Auto-format, run tests, log actions |
| Notification | When Claude sends a notification | No | Desktop alerts, Slack messages |
| Stop | When Claude finishes a response | No | Cleanup, summary generation |
| SessionStart | At session initialization | No | Inject context, set environment |
| UserPromptSubmit | When user submits a prompt | Yes (exit 2) | Input validation, content filtering |
| PreCompact | Before context compaction | No | Save state before memory is trimmed |
| PostCompact | After context compaction | No | Re-inject critical context |
| ConfigChange | When settings change | No | Hot-reload environment variables |
| FileChanged | When a watched file changes | No | Trigger rebuilds, invalidate caches |
| TaskCreated | When a new task is spawned | No | Task tracking, resource allocation |
| PermissionDenied | When a permission check fails | No | Audit logging, alert on blocked actions |
| WorktreeCreate | When a new Git worktree is created | No | Initialize worktree-specific settings |
| SubagentStart | When a subagent spawns | No | Monitor subagent activity |
| SubagentStop | When a subagent completes | No | Validate 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.
| Type | Speed | Complexity | Best For | Example |
|---|---|---|---|---|
| Command | Fast | Low | Formatting, blocking, logging | Run Prettier after file edit |
| HTTP | Medium | Medium | External services, webhooks | POST to Slack on completion |
| Prompt | Slow | Medium | Subjective decisions | "Is this code safe to run?" |
| Agent | Slowest | High | Complex file-aware verification | Check 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.
{
"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).
{
"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.
{
"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.
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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)
{
"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)
{
"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
| Mechanism | Deterministic? | When It Runs | Best For | Example |
|---|---|---|---|---|
| Hooks | Yes | Automatically on lifecycle events | Enforcement, automation, notifications | Auto-format, block file writes |
| MCP | No (Claude decides) | When Claude calls the MCP tool | New capabilities, external data access | Query a database, search Notion |
| Skills | No (user triggers) | When user invokes a slash command | Reusable instruction sets | /review for code review workflow |
| CLAUDE.md | No (guidance) | Read at session start | Project 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
{
"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
| Stack | Format Command | Test Command | Watch Extensions |
|---|---|---|---|
| Node/TypeScript | npx prettier --write | npx jest --no-coverage | .ts, .tsx, .js, .jsx |
| Python | black | pytest -x | .py |
| Go | gofmt -w | go test ./... | .go |
| Rust | rustfmt | cargo 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:
/hookscommand, Type/hooksin Claude Code to see all registered hooks, their matchers, and their status.- Transcript inspection, After a hook fires, check the session transcript. Hook executions appear with their output and exit code.
- Quick toggle, Add
"disableAllHooks": trueto your settings.json to temporarily disable all hooks without deleting the config. Remove it (or set tofalse) 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 Event | Interactive Mode | Headless Mode (-p) | CI Recommendation |
|---|---|---|---|
| PreToolUse (exit 2) | Blocks, shows message | Pauses for --resume | Use for mandatory human approvals |
| PostToolUse | Runs normally | Runs normally | Keep formatters and loggers |
| Notification | Desktop alert | Still fires (no UI) | Redirect to log file or Slack webhook |
| Stop | Runs cleanup | Runs cleanup | Good for CI artifact collection |
| SessionStart | Injects context | Injects context | Inject 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:
- 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 theWritetool. Check exact tool names with/hooks. - Wrong settings file, Hooks in
~/.claude/settings.jsonwon't appear in/hooksoutput for the project scope. Try.claude/settings.jsonin 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 2to 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
iffield.
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
timeoutfield (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.