If you are using Claude Code without hooks, you are leaving a massive chunk of productivity on the table. I spent the last several months building, testing, and breaking Claude Code hooks across multiple production projects, and I can say with confidence that hooks fundamentally changed how I work with AI-assisted development.
Claude Code hooks are user-defined automation scripts that execute at specific lifecycle points during an AI coding session. They let you run shell commands, call HTTP endpoints, fire LLM prompts, or spawn sub-agents automatically — before a tool runs, after a file is edited, when the agent stops, or when a new session starts. The result is deterministic control over a system that is otherwise probabilistic.
This guide covers everything I have learned about claude code hooks: what they are, how to set them up, every hook event available, four handler types, real configuration examples you can copy-paste, performance considerations, and the mistakes I made so you do not have to. Whether you are new to Claude Code or already running it in production, this article will level up your automation game.
|
Quick Answer Claude Code hooks are automated triggers defined in your settings.json that fire shell commands, HTTP requests, LLM prompts, or sub-agents at specific lifecycle events like PreToolUse, PostToolUse, Stop, and SessionStart. They give you guaranteed, deterministic enforcement of dev standards — unlike CLAUDE.md rules which are suggestions the model can ignore. Set them up once, and they run every single time without manual intervention. |
What Are Claude Code Hooks and Why Do They Matter?
The Core Problem Hooks Solve
Claude Code writes solid code, but it does not always remember to follow your formatting rules, run your test suite, or avoid touching sensitive files. You end up typing the same instructions over and over — “run prettier,” “do not modify .env,” “execute tests before finishing.” Hooks eliminate this entirely.
I learned this the hard way. On one project, Claude Code reformatted files I explicitly asked it not to touch. On another, it decided it was “done” when half the acceptance criteria were unmet. Hooks fixed both problems permanently. A PreToolUse hook now blocks writes to protected files every time. A Stop hook forces verification before the agent can finish. No more babysitting.
Hooks vs. CLAUDE.md vs. MCP: When to Use What
This is the distinction most articles miss. CLAUDE.md instructions are suggestions — the model usually follows them, but it is not guaranteed. Hooks are deterministic rules that always execute. MCP servers extend Claude Code with external tools and data sources.
|
Feature |
CLAUDE.md |
Hooks |
MCP Servers |
|
Execution |
Probabilistic (LLM decides) |
Deterministic (always runs) |
On-demand (tool call) |
|
Best for |
Guidelines, preferences, style |
Hard rules, automation, blocking |
External integrations, data |
|
Enforcement |
Can be ignored by model |
Cannot be bypassed |
Called when model chooses |
|
Setup |
Markdown file |
JSON + shell scripts |
Server config + protocol |
|
Example |
“Prefer Bun over npm” |
“Always format with Prettier” |
“Search Jira tickets” |
My rule of thumb: if breaking the rule would cause a real problem (security, formatting, deployment), use a hook. If it is a preference the model can reasonably override, put it in CLAUDE.md. If it needs external data, use MCP.
How Claude Code Hooks Work: Architecture and Lifecycle
The Hook Execution Flow
Every Claude Code session follows a lifecycle: session starts, user submits a prompt, tools execute, agent responds, session ends. Hooks let you inject custom logic at any point in that cycle. The flow looks like this:
Claude Code Event Fires → Matcher Evaluation → If Condition Check → Hook Handler Executes → Result Returned
When an event fires, Claude Code checks all configured hook groups for that event. If a matcher matches (or no matcher is set, meaning match everything), it evaluates the optional “if” condition. If that passes too, the hook handler runs. Your handler receives JSON context on stdin, performs its logic, and communicates results through exit codes and stdout.
Four Hook Handler Types
As of 2026, Claude Code supports four distinct handler types, each suited to different automation patterns:
|
Handler Type |
How It Works |
Best For |
|
Command (type: “command”) |
Runs a shell command. Receives JSON on stdin, returns exit code + optional JSON on stdout. |
Local validation, formatting, file checks, git operations |
|
HTTP (type: “http”) |
Sends a POST request with JSON body to a URL endpoint. Response body uses same output format as command hooks. |
External service integration, webhooks, remote APIs, team dashboards |
|
Prompt (type: “prompt”) |
Sends a prompt to a Claude model for single-turn yes/no evaluation. Returns JSON decision. |
Semantic validation, checking if work is complete, nuanced rule enforcement |
|
Agent (type: “agent”) |
Spawns a sub-agent with tool access (Read, Grep, Glob). Multi-step reasoning before decision. |
Deep verification, running test suites, complex multi-file validation |
I use command hooks for 80% of my automation — they are fast, simple, and cover most use cases. I reserve prompt hooks for Stop event verification where I need the model to evaluate whether all tasks were completed. Agent hooks are powerful but slower; I only use them when I need the hook to actually read files or run grep to make a decision.
Every Claude Code Hooks Event Explained
Claude Code currently exposes over 20 lifecycle events. Here are the ones I use most, with the full list in the reference table below.
1. SessionStart
Fires when a session begins, resumes, clears, or compacts. I use this to inject development context — loading git status, recent issues, and environment variables so Claude starts every session with full project awareness. The matcher supports startup, resume, clear, and compact to differentiate start conditions.
2. UserPromptSubmit
Fires when you submit a prompt, before Claude processes it. I use this for two things: validating that prompts include enough context (exit 2 blocks the prompt), and injecting additional context like the current branch name or recent commits. The stdout from this hook is added as context that Claude can see and act on.
3. PreToolUse
This is the most powerful event. It fires before every tool call and can block execution entirely. I have PreToolUse hooks that block writes to .env files, prevent destructive bash commands (rm -rf, DROP TABLE, push –force), and warn about modifications to sensitive files like private keys. The matcher filters by tool name — Bash, Write, Edit, Read — so your hook only fires for relevant operations.
4. PostToolUse
Fires after a tool call succeeds. The classic use case is auto-formatting: every time Claude writes or edits a file, Prettier or Black runs automatically. I also use PostToolUse to auto-stage modified files in git, which saves me from manually adding them later.
5. Stop
Fires when Claude finishes responding. If your hook returns exit code 2, Claude is forced to continue working. This is game-changing for autonomous workflows — combine it with a prompt hook that evaluates task completion, and you get an AI agent that self-verifies its work before stopping.
6. Notification
Fires when Claude needs your attention — permission prompts, idle prompts, auth success. I use this to send desktop notifications on macOS via osascript and Slack messages via webhook URL. No more checking the terminal every 30 seconds during long-running tasks.
7. PreCompact and PostCompact
PreCompact fires before context compaction (when Claude shrinks its context window to save tokens). I use this to back up the current transcript before compaction wipes it. PostCompact fires after, which is useful for re-injecting critical context that may have been lost.
Complete Hook Events Reference
|
Event |
When It Fires |
Can Block? |
Common Use Case |
|
SessionStart |
Session begins/resumes/clears |
No |
Inject dev context, set env vars |
|
InstructionsLoaded |
CLAUDE.md or rules file loaded |
No |
Monitor config changes |
|
UserPromptSubmit |
User submits a prompt |
Yes (exit 2) |
Validate/enrich prompts |
|
PreToolUse |
Before a tool executes |
Yes (exit 2) |
Block dangerous ops, protect files |
|
PermissionRequest |
Permission dialog appears |
Yes |
Auto-approve/deny specific tools |
|
PermissionDenied |
Tool call denied by classifier |
No |
Retry with {retry: true} |
|
PostToolUse |
After tool succeeds |
No |
Auto-format, git stage, run tests |
|
PostToolUseFailure |
After tool fails |
No |
Error logging, retry logic |
|
Notification |
Claude needs attention |
No |
Desktop/Slack notifications |
|
SubagentStart |
Sub-agent spawns |
No |
Setup for sub-agents |
|
SubagentStop |
Sub-agent finishes |
No |
Cleanup, branch handling |
|
TaskCreated |
Task being created |
Yes |
Validate task parameters |
|
TaskCompleted |
Task marked complete |
Yes |
Verify task output |
|
Stop |
Claude finishes responding |
Yes (exit 2) |
Force task completion verification |
|
StopFailure |
Turn ends due to API error |
No |
Error logging |
|
TeammateIdle |
Agent team member goes idle |
Yes |
Reassign work |
|
PreCompact |
Before context compaction |
No |
Transcript backup |
|
PostCompact |
After compaction completes |
No |
Re-inject critical context |
|
FileChanged |
Watched file changes on disk |
No |
Hot-reload configs |
|
CwdChanged |
Working directory changes |
No |
Environment management (direnv) |
|
WorktreeCreate |
Worktree being created |
No |
Custom git worktree setup |
|
WorktreeRemove |
Worktree being removed |
No |
Cleanup worktree resources |
|
SessionEnd |
Session terminates |
No |
Cleanup, final logging |
How to Set Up Claude Code Hooks: Step-by-Step
Configuration File Locations
Hooks are defined in JSON settings files. You have three levels to choose from, and understanding which to use matters for team workflows:
|
Level |
File Path |
Scope |
When to Use |
|
User-level |
~/.claude/settings.json |
Applies to all projects for your user |
Personal prefs: notifications, global safety blocks |
|
Project-level |
.claude/settings.json (in project root) |
Shared via git with your team |
Team standards: formatting, linting, branch guards |
|
Project-local |
.claude/settings.local.json |
Project-specific, not committed |
Personal project overrides, experimental hooks |
I keep dangerous-command blocking and notification hooks at the user level so they apply everywhere. Formatting and test-running hooks go in the project-level file so the entire team benefits. My rule: if it is a personal preference, use user-level. If the team should follow it, commit it to the project.
Your First Hook: Auto-Format on Every File Edit
This is the most common starting point and takes two minutes. Add this to your settings.json:
{
“hooks”: {
“PostToolUse”: [
{
“matcher”: “Write|Edit|MultiEdit”,
“hooks”: [
{
“type”: “command”,
“command”: “jq -r ‘.tool_input.file_path’ | xargs npx prettier –write 2>/dev/null; exit 0”
}
]
}
]
}
}
That is it. Every file Claude writes or edits now gets auto-formatted with Prettier. The matcher “Write|Edit|MultiEdit” ensures the hook only fires for file-modifying tools, not reads or bash commands. The exit 0 at the end ensures the hook never blocks Claude — formatting failures are non-critical.
Blocking Dangerous Commands with PreToolUse
This is the hook I set up on every machine I use. It blocks destructive bash commands before they execute:
#!/bin/bash
# .claude/hooks/block-dangerous.sh
CMD=$(jq -r ‘.tool_input.command’ < /dev/stdin)
for pattern in “rm -rf /” “rm -rf ~” “DROP TABLE” “push.*–force”; do
if echo “$CMD” | grep -qiE “$pattern”; then
echo “Blocked: $CMD” >&2
exit 2
fi
done
exit 0
Exit code 2 is the key here. It tells Claude Code to block the tool call and feed the stderr message back to Claude as an error. Claude will then attempt an alternative approach. Any other non-zero exit code is a non-blocking error — the operation continues.
Desktop Notifications When Claude Finishes
If you run long tasks and switch to another window, this hook pings you when Claude needs attention. On macOS:
{
“hooks”: {
“Notification”: [
{
“matcher”: “”,
“hooks”: [
{
“type”: “command”,
“command”: “osascript -e ‘display notification \”Claude Code needs attention\” with title \”Claude Code\”‘”
}
]
}
]
}
}
On Linux, replace osascript with notify-send. On Windows, use PowerShell toast notifications. The empty matcher means fire on every notification event.
Matchers, JSON Input/Output, and Exit Codes
How Matchers Work
Matchers are regex strings that filter which actions trigger a hook. They are case-sensitive and match against tool names or event subtypes. Here are the patterns I use daily:
|
Matcher |
What It Matches |
Use Case |
|
Bash |
Only Bash tool calls |
Block dangerous commands |
|
Write|Edit|MultiEdit |
Any file-modifying tool |
Auto-format after edits |
|
Read|Edit|Write|Bash |
Most common tools |
Protect sensitive files from all access |
|
mcp__github__.* |
All GitHub MCP tools |
Log or validate GitHub operations |
|
mcp__.* |
All MCP server tools |
Audit all external integrations |
|
“” (empty) or .* |
Everything |
Global logging, universal notifications |
|
Notebook.* |
All Notebook tools |
Notebook-specific automation |
The “if” field adds a second filter layer using a lightweight pattern syntax like Bash(rm *) that matches against tool input. This lets you skip the overhead of spawning a process for hooks that only apply to specific commands.
JSON Input Structure
Every hook receives a JSON payload on stdin with context about the event. Here is what a typical PreToolUse payload for a Bash command looks like:
{
“session_id”: “abc123”,
“cwd”: “/your/project”,
“hook_event_name”: “PreToolUse”,
“tool_name”: “Bash”,
“tool_input”: {
“command”: “npm test”
}
}
For Write tool calls, tool_input contains file_path and content. For Edit, it includes file_path, old_string, and new_string. You parse these with jq in shell scripts or json.loads() in Python.
Exit Code Reference
The exit code from your hook tells Claude Code what to do next. Getting this wrong is the most common source of hook bugs:
|
Exit Code |
Meaning |
Stdout Parsed? |
Effect |
|
0 |
Success |
Yes (JSON output) |
Action proceeds; stdout shown in verbose mode (Ctrl+O) |
|
2 |
Blocking error |
No (ignored) |
Action blocked; stderr fed to Claude as error message |
|
Any other |
Non-blocking error |
No (ignored) |
Action continues; stderr shown in verbose mode |
Advanced Claude Code Hooks Patterns I Use in Production
Stop Hook with Self-Verification (Prompt Hook)
This is the pattern that changed my autonomous workflow. Instead of a shell command, this Stop hook uses a prompt handler that asks a Claude model to evaluate whether the task was actually completed:
{
“hooks”: {
“Stop”: [
{
“hooks”: [
{
“type”: “prompt”,
“prompt”: “Review the conversation. Did the user request get fully completed? Check: all files created, tests passing, no TODO left. Respond with {\”ok\”: true} if done, or {\”ok\”: false, \”reason\”: \”what remains\”} if not.”,
“timeout”: 30
}
]
}
]
}
}
If the model returns {“ok”: false}, Claude Code continues working on the unfinished items. This effectively creates a self-verifying agent.
|
Warning: Stop Hook Infinite Loop When a Stop hook returns exit 2 (or ok: false for prompt hooks), Claude continues working and will eventually try to stop again — triggering the same hook. If your verification logic never passes, you get an infinite loop. Always check the stop_hook_active field in the JSON input. When it is true, your hook is being called on a subsequent stop attempt, and you should exit 0 to let Claude finish. |
Protecting Sensitive Files from All Access
This PreToolUse hook blocks reads, writes, and bash access to any file matching sensitive patterns. I use it to prevent Claude from ever touching credentials, private keys, or env files:
#!/bin/bash
# .claude/hooks/protect-secrets.sh
INPUT=$(cat)
FILE=$(echo “$INPUT” | jq -r ‘.tool_input.file_path // .tool_input.command // “”‘)
if echo “$FILE” | grep -qiE ‘\.env$|\.env\.|\.pem$|\.key$|id_rsa|credentials’; then
echo “Access to sensitive file blocked” >&2
exit 2
fi
exit 0
Auto-Commit with GitButler Integration
If you run multiple Claude Code sessions simultaneously, GitButler hooks can automatically isolate each session into its own virtual branch and commit changes per session. The configuration registers PreToolUse, PostToolUse, and Stop hooks that communicate with GitButler CLI:
{
“hooks”: {
“PreToolUse”: [{ “matcher”: “Edit|MultiEdit|Write”, “hooks”: [{ “type”: “command”, “command”: “but claude pre-tool” }] }],
“PostToolUse”: [{ “matcher”: “Edit|MultiEdit|Write”, “hooks”: [{ “type”: “command”, “command”: “but claude post-tool” }] }],
“Stop”: [{ “matcher”: “”, “hooks”: [{ “type”: “command”, “command”: “but claude stop” }] }]
}
}
With this setup, three parallel Claude Code sessions will each produce commits on separate branches automatically. No merge conflicts, no manual branch management.
Context Re-Injection After Compaction
Claude Code compacts its context window to save tokens, and sometimes critical instructions from CLAUDE.md get lost in the process. This UserPromptSubmit hook re-injects your core rules on every prompt:
{
“hooks”: {
“UserPromptSubmit”: [
{
“matcher”: “”,
“hooks”: [
{
“type”: “command”,
“command”: “cat $CLAUDE_PROJECT_DIR/CLAUDE.md”
}
]
}
]
}
}
The stdout from UserPromptSubmit hooks is added as context that Claude sees. This means your CLAUDE.md rules are always fresh in context, even after compaction.
Running Tests Asynchronously After File Changes
For hooks that take a long time (like running a full test suite), use the async: true flag so the hook runs in the background without blocking Claude:
{
“hooks”: {
“PostToolUse”: [
{
“matcher”: “Write|Edit”,
“hooks”: [
{
“type”: “command”,
“command”: “$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh”,
“async”: true
}
]
}
]
}
}
Async hooks fire and forget — they cannot block tool execution or return decisions. Use them for logging, notifications, and background processes where you do not need the result before Claude continues.
Using Claude Code Hooks in CI/CD and Headless Mode
Hooks work identically in headless mode as in interactive mode. This makes them powerful for CI/CD pipelines where Claude Code runs automated tasks. Your PreToolUse safety hooks still block dangerous commands, PostToolUse hooks still log actions, and SessionStart hooks still initialize the environment.
The $CLAUDE_CODE_REMOTE environment variable is set when Claude Code runs in a remote or headless environment. I use it to conditionally skip desktop notifications while keeping Slack webhooks active:
#!/bin/bash
if [ -n “$CLAUDE_CODE_REMOTE” ]; then
# Headless: send Slack webhook instead
curl -s -X POST -H “Content-Type: application/json” \
-d ‘{“text”:”Claude Code task complete”}’ “$SLACK_WEBHOOK_URL”
else
# Local: desktop notification
osascript -e ‘display notification “Done” with title “Claude Code”‘
fi
Debugging Claude Code Hooks
The /hooks Menu
Type /hooks inside Claude Code to open a read-only browser showing all configured hooks. It lists every event with a count of configured hooks, lets you drill into matchers, and shows the full details of each handler. I use this to verify my configuration is loaded correctly, especially when hooks in project-level vs user-level files conflict.
Verbose Mode (Ctrl+O)
Toggle verbose mode with Ctrl+O inside Claude Code. In verbose mode, stdout and stderr from all hooks are displayed in the terminal. This is essential for debugging — without it, hook output is silent and you have no visibility into what is happening.
Common Debugging Mistakes
|
Mistake |
Symptom |
Fix |
|
Reading tool_input from env vars |
Hook gets empty data, silently fails |
Always read JSON from stdin, not env vars |
|
Forgetting exit 0 on success |
Non-zero exit treated as error |
Explicitly exit 0 at end of script |
|
Using exit 2 in PostToolUse |
Exit 2 has no blocking effect here |
Exit 2 only blocks in PreToolUse, UserPromptSubmit, Stop |
|
Hook script not executable |
Permission denied errors |
Run chmod +x on your hook scripts |
|
Forgetting jq dependency |
Parse errors on first run |
Install jq: brew install jq / apt install jq |
|
Stop hook infinite loop |
Claude never finishes |
Check stop_hook_active field, exit 0 if true |
Claude Code Hooks vs. Other AI Coding Agent Hooks
Claude Code is not the only tool with hook support in 2026. Here is how the landscape compares:
|
Feature |
Claude Code |
Cursor |
GitHub Copilot |
|
Handler types |
4 (command, HTTP, prompt, agent) |
Command only |
Command only |
|
Lifecycle events |
20+ |
5 (beforeShellExecution, beforeMCPExecution, beforeReadFile, afterFileEdit, stop) |
Limited (preToolUse focus) |
|
Prompt/LLM hooks |
Yes (prompt + agent types) |
No |
No |
|
Async execution |
Yes (async: true flag) |
No |
No |
|
MCP tool matching |
Yes (mcp__server__.*) |
Yes (beforeMCPExecution) |
No |
|
CI/CD headless mode |
Full support |
No |
Limited |
|
Configuration |
JSON settings files (3 levels) |
JSON |
.github/hooks/*.json |
The key differentiator for Claude Code is handler diversity. Command hooks handle straightforward checks. Prompt hooks handle semantic evaluation. Agent hooks handle deep analysis. This three-tier system maps naturally to different development requirements, from simple formatting to complex quality gate enforcement.
Frequently Asked Questions About Claude Code Hooks
1. Do Claude Code hooks slow down the coding workflow?
Command hooks add negligible overhead — typically a few milliseconds per invocation. Prompt hooks are slower at 2-8 seconds because they call a Claude model. Agent hooks can take 10-30 seconds for complex verification. Place slow hooks on low-frequency events like Stop (fires once per task) rather than high-frequency events like PreToolUse (fires on every tool call).
2. Can I use hooks without knowing shell scripting?
You need basic shell knowledge for command hooks since they run bash/shell commands. However, you can ask Claude Code itself to write hooks for you — describe what you want in the CLI, and Claude will generate the settings.json configuration and hook scripts. HTTP hooks only need a URL endpoint. Prompt hooks need zero code, just a natural language prompt string.
3. How many hooks can I configure per event?
There is no hard limit. You can register multiple matcher groups under the same event, and each group can have multiple handlers. All matching hooks execute — they do not short-circuit on the first match. However, be mindful of performance: ten synchronous hooks on PreToolUse each taking 500ms means 5 seconds of delay before every tool call.
4. Do hooks work with MCP server tool calls?
Yes. Use matchers like mcp__github__.* to target specific MCP server tools, or mcp__.* to match all MCP tool calls. PreToolUse hooks can block MCP operations, PostToolUse hooks can log them, and you get the same JSON input/output format as with built-in tools.
5. Where should I put hooks — user-level or project-level settings?
User-level (~/.claude/settings.json) for personal preferences that apply everywhere: notifications, global safety blocks, personal formatting. Project-level (.claude/settings.json) for team standards committed to git: shared formatting rules, linting, branch guards, test requirements. Project-local (.claude/settings.local.json) for personal project-specific overrides that should not be committed.
Conclusion
Claude Code hooks transform the AI coding assistant from a tool you have to supervise into an automated development environment that enforces your standards consistently. After months of using them across multiple production projects, I consider hooks non-negotiable for any serious Claude Code workflow.
Start with three hooks — auto-formatting with PostToolUse, dangerous command blocking with PreToolUse, and notifications with the Notification event. That setup takes ten minutes and immediately saves hours of repetitive work. Once you see the value, expand into Stop hooks for self-verification, SessionStart for context injection, and async hooks for background test execution.
The distinction between CLAUDE.md suggestions and hook enforcement is the single most important concept here. CLAUDE.md is a request. A hook is a guarantee. For anything that matters — security, formatting, deployment safety, task verification — use a hook.
|
Pro Tip Use the /hooks menu inside Claude Code to verify your configuration is loaded correctly. Combine it with Ctrl+O verbose mode to see real-time hook output. And always test new hooks on a non-critical project first — a misconfigured Stop hook can trap Claude in an infinite loop that is surprisingly hard to break out of. |


