Claude Code hooks let you run arbitrary code at specific points in the AI’s lifecycle. Format files automatically after every edit, block dangerous shell commands before they run, get notified when Claude needs input, inject fresh context after compaction — hooks make all of this deterministic.
The key word is deterministic. Without hooks, you are relying on Claude to remember to run your formatter, to avoid certain patterns, to reload context after a long session. Hooks remove that reliance. They run unconditionally, every time.
This guide covers the complete hooks system and gives you 10 patterns you can drop into your own projects today.
How Hooks Work
Hooks are shell commands (or HTTP endpoints, or LLM prompts) configured in a settings file. When Claude Code hits a specific lifecycle event, it fires the matching hooks, passes JSON data to stdin, and reads your response from stdout, stderr, and the exit code.
The exit code controls what happens next:
- Exit 0 — allow the action to proceed. Anything written to stdout gets injected into Claude’s context (for
UserPromptSubmitandSessionStartevents). - Exit 2 — block the action. Write a reason to stderr; Claude receives it as feedback and adjusts.
- Any other exit code — the action proceeds, but stderr is logged silently. Use
Ctrl+Oto see these messages.
For more nuanced control, exit 0 and write a JSON object to stdout instead of using exit codes alone.
Hook Events Reference
As of 2026, Claude Code supports 25 hook events:
| Event | When it fires |
|---|---|
SessionStart | Session begins or resumes |
SessionEnd | Session terminates |
UserPromptSubmit | You submit a prompt, before Claude processes it |
PreToolUse | Before a tool call executes (can block) |
PostToolUse | After a tool call succeeds |
PostToolUseFailure | After a tool call fails |
PermissionRequest | When a permission dialog would appear |
PermissionDenied | When a tool call is denied by the auto-mode classifier |
Notification | When Claude sends a notification |
Stop | When Claude finishes responding |
StopFailure | When a turn ends due to API error |
SubagentStart | When a subagent is spawned |
SubagentStop | When a subagent finishes |
TaskCreated | When a task is created via TaskCreate |
TaskCompleted | When a task is marked complete |
TeammateIdle | When an agent team teammate goes idle |
InstructionsLoaded | When a CLAUDE.md or rules file is loaded |
ConfigChange | When a configuration file changes during a session |
CwdChanged | When the working directory changes |
FileChanged | When a watched file changes on disk |
WorktreeCreate | When a worktree is being created |
WorktreeRemove | When a worktree is being removed |
PreCompact | Before context compaction |
PostCompact | After context compaction completes |
Elicitation | When an MCP server requests user input |
ElicitationResult | After a user responds to an MCP elicitation |
The most commonly used events are PreToolUse, PostToolUse, SessionStart, Stop, and Notification.
Configuration
Hooks go in a hooks block inside a settings file. Two locations:
// ~/.claude/settings.json — applies to all your projects
// .claude/settings.json — applies to this project, committable to git
Basic structure:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "your-command-here"
}
]
}
]
}
}
Each event takes an array of hook groups. Each group has a matcher (regex against the tool name, or event-specific field) and a hooks array. Use /hooks in Claude Code to browse all configured hooks and verify they appear correctly.
10 Real-World Hook Patterns
1. Auto-Format on Every File Edit
Run Prettier automatically after every file edit. Zero configuration required on Claude’s end.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true"
}
]
}
]
}
}
Why || true? Prettier exits non-zero on unsupported file types (like .sh). Without || true, the hook would block every edit to non-JS files. Silently ignore unsupported types.
For Python, swap Prettier for Black:
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I{} sh -c 'echo {} | grep -q \"\\.py$\" && black {} 2>/dev/null || true'"
}
2. Block Dangerous Shell Commands
Prevent Claude from running commands that can cause irreversible damage. This runs before every Bash tool call.
Save this to .claude/hooks/guard-bash.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
BLOCKED_PATTERNS=(
"rm -rf /"
"rm -rf \*"
"git push --force"
"git push -f"
"DROP TABLE"
"DROP DATABASE"
"chmod -R 777"
"> /dev/sda"
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "Blocked: command matches dangerous pattern '$pattern'" >&2
exit 2
fi
done
exit 0
Register it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-bash.sh"
}
]
}
]
}
}
Important: exit 2 here blocks even in bypassPermissions mode. Hooks can enforce rules that users cannot bypass by changing permission settings.
3. Protect Sensitive Files from Edits
Block Claude from touching .env, lock files, and anything in .git/. Claude receives the reason and adjusts its approach.
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(
".env"
".env.local"
".env.production"
"package-lock.json"
"yarn.lock"
"pnpm-lock.yaml"
".git/"
"*.pem"
"*.key"
)
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH is a protected file. Do not modify it." >&2
exit 2
fi
done
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
4. Desktop Notifications When Claude Needs Input
Stop watching the terminal. Get a native notification whenever Claude is waiting for you.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
For Linux:
{
"type": "command",
"command": "notify-send 'Claude Code' 'Needs your attention'"
}
Gotcha on macOS: osascript routes through Script Editor, which needs notification permission. If nothing appears, run the command once manually in Terminal, then go to System Settings > Notifications > Script Editor and enable notifications.
5. Re-Inject Context After Compaction
When Claude’s context window fills up, compaction summarizes the conversation. Important details can get lost. This hook fires right after compaction and re-injects whatever you need Claude to remember.
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Context restored after compaction. Key constraints: use Bun (not npm), run bun test before every commit, never modify src/generated/. Current branch: $(git branch --show-current)'"
}
]
}
]
}
}
Anything your command writes to stdout gets added to Claude’s context as a system reminder. You can make this dynamic — pull from a file, run git log --oneline -5, output environment status. The hook only fires on compact events, not regular session starts.
6. Run Tests After Relevant File Changes
Run the test suite automatically after Claude edits source files. Catch regressions immediately without asking Claude to remember.
#!/bin/bash
# .claude/hooks/run-tests.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only run for source files, not tests themselves or config
if echo "$FILE_PATH" | grep -qE '\.(ts|tsx|js|jsx|py)$' && \
! echo "$FILE_PATH" | grep -qE '\.(test|spec)\.' && \
! echo "$FILE_PATH" | grep -q '__tests__'; then
echo "Running tests for changed file..." >&2
if ! npm test --silent 2>&1 | tail -5; then
echo "Tests failed after editing $FILE_PATH" >&2
exit 2
fi
fi
exit 0
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"timeout": 120,
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests.sh"
}
]
}
]
}
}
Note the timeout: 120 — the default hook timeout is 10 minutes (600 seconds), but you can tune it per hook.
7. Enforce Commit Message Format
Block git commit calls that don’t follow Conventional Commits format. Use the if field (requires Claude Code v2.1.85+) to narrow the hook to git commits only, without spawning a process for every other Bash command.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git commit*)",
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command'); if echo \"$CMD\" | grep -q 'git commit'; then MSG=$(echo \"$CMD\" | grep -oP '(?<=-m \")[^\"]+'); if ! echo \"$MSG\" | grep -qP '^(feat|fix|chore|docs|style|refactor|test|perf|ci)(\\(.+\\))?: .+'; then echo \"Commit message must follow Conventional Commits format: type(scope): description\" >&2; exit 2; fi; fi; exit 0"
}
]
}
]
}
}
For readability, move the logic to a script file instead of inlining.
8. Audit All Tool Usage to a Log File
Keep a persistent log of every tool Claude uses during a session. Useful for debugging, auditing, or understanding Claude’s behavior in complex tasks.
{
"hooks": {
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{ts: now | todate, tool: .tool_name, input: .tool_input}' >> ~/.claude/tool-audit.log"
}
]
}
]
}
}
Empty matcher means “fire on every tool use.” The log format is one JSON object per line (JSONL), easy to grep or pipe to jq.
To view recent tool calls:
tail -20 ~/.claude/tool-audit.log | jq '.tool + ": " + (.input | tostring)'
9. Auto-Approve Plan Mode Exit
When you use Plan Mode, Claude presents a plan and then waits for approval before executing. If you always want to proceed, skip the dialog entirely with a PermissionRequest hook.
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}
Keep the matcher scoped to ExitPlanMode. Using an empty matcher here would auto-approve every permission prompt, including file writes and shell commands.
10. Use an LLM to Check Task Completion
When Claude signals it’s done, use a Stop hook with type: "prompt" to ask an LLM whether all tasks are actually complete. If not, Claude receives the reason and keeps working.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Review the conversation. Are all tasks requested by the user fully complete, including tests and documentation? If anything is missing, respond with {\"ok\": false, \"reason\": \"what needs to be done\"}. If everything is done, respond with {\"ok\": true}."
}
]
}
]
}
}
Infinite loop prevention: If your Stop hook returns ok: false, Claude continues working and will trigger another Stop event when it finishes. To prevent loops, parse the stop_hook_active field from stdin and exit 0 if it’s true. For command hooks:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
# rest of your logic
Prompt hooks handle this automatically — they only fire when stop_hook_active is false.
Beyond Command Hooks
Most examples above use "type": "command" — a shell script. But there are three other hook types worth knowing:
type: "prompt" — sends your prompt and the hook input to a Claude model (Haiku by default). Returns {"ok": true} or {"ok": false, "reason": "..."}. Use it when the decision requires judgment that a regex can’t make. See example 10 above.
type: "agent" — spawns a full subagent with tool access. The agent can read files, run commands, check test output, and make a multi-step determination before returning a decision. Use it when type: "prompt" isn’t enough because you need to inspect the actual state of the codebase.
type: "http" — POSTs event data to a URL. The endpoint receives the same JSON as a command hook would on stdin, and returns results in the HTTP response body. Useful for shared audit services or team-wide enforcement endpoints.
Common Gotchas
Shell profile pollution. Claude Code sources ~/.zshrc or ~/.bashrc before running hook commands. If your profile has unconditional echo statements, that output gets prepended to your hook’s JSON output and breaks parsing. Wrap echoes in an interactive-shell check:
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
Absolute paths. Hook commands run in the working directory with Claude’s environment. If your hook script isn’t on $PATH, use an absolute path or $CLAUDE_PROJECT_DIR:
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/my-hook.sh"
Parallel hook execution. When multiple hooks match the same event, they all run in parallel. If two PreToolUse hooks both try to modify the tool’s input (using updatedInput), the last one to finish wins — non-deterministically. Don’t have multiple hooks rewrite the same tool’s input.
PostToolUse can’t undo. The tool has already executed. Use PostToolUse for side effects (formatting, logging) and PreToolUse for enforcement.
PermissionRequest doesn’t fire in headless mode. If you’re running Claude with -p (non-interactive), use PreToolUse for automated permission decisions instead.
Debugging Hooks
When a hook isn’t firing or behaving unexpectedly:
- Run
/hooksinside Claude Code — verify the hook appears under the correct event with the right matcher. - Toggle verbose mode with
Ctrl+Oto see hook output inline in the transcript. - Test the hook manually by piping sample JSON:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"},"session_id":"test","cwd":"/tmp"}' | ./.claude/hooks/my-hook.sh
echo "Exit code: $?"
- Run
claude --debugfor full execution details including which hooks matched.
Where to Go from Here
The 10 patterns above cover the most common use cases. The hooks system is much deeper — HTTP hooks, agent hooks, worktree hooks, MCP elicitation hooks, and the full input/output schema are documented in the Hooks reference.
For ready-to-use examples from the community, the karanb192/claude-code-hooks repository is a growing collection worth bookmarking.
And if you’re building out your Claude Code setup more broadly, our gallery has hundreds of real CLAUDE.md and AGENTS.md configurations from production codebases to learn from.