Hooks run outside the LLM loop.
Deterministic. Zero token cost. Total control.
Hooks intercept every tool call, session event, and notification before or after it happens. No AI reasoning involved. Pure logic gates you define.
The Complete Event System
Every hook fires on a specific lifecycle event. 8 events cover the entire Claude Code runtime, from session start to teardown.
| Event | Fires When | Use For |
|---|---|---|
| PreToolUse | Before any tool executes | Block operations, inject context, validate inputs |
| PostToolUse | After any tool completes | Lint code, format output, log changes |
| SessionStart | Session opens | Load dynamic context, check environment |
| Stop | Claude finishes responding | Notify user, log cost, trigger downstream |
| Notification | Claude needs user input | Sound alert, system notification |
| InstructionsLoaded | CLAUDE.md files loaded | Debug which rules loaded, log active config |
| WorktreeCreate | New worktree created | Custom initialization, env setup |
| WorktreeRemove | Worktree removed | Custom teardown, cleanup temp files |
The Exit Code System
Your hook's exit code tells Claude what to do next. Three possible outcomes, each with different behavior.
#!/bin/bash # Example: block dangerous bash commands COMMAND="$CLAUDE_TOOL_INPUT_COMMAND" if echo "$COMMAND" | grep -qE "rm -rf|git push --force|git reset --hard"; then echo "BLOCKED: dangerous command detected" >&2 exit 2 # Hard block fi exit 0 # Allow everything else
Context Injection
When a hook returns JSON to stdout, Claude reads it as a message. This creates a feedback loop where your tools inform the AI without spending tokens on reasoning.
#!/bin/bash # PostToolUse hook: auto-lint after edits FILE="$CLAUDE_TOOL_INPUT_FILE_PATH" # Only lint JS/TS files if [["$FILE" =~ \.(js|ts|jsx|tsx)$ ]]; then RESULT=$(npx eslint "$FILE" 2>&1) if [ $? -ne 0 ]; then # Return JSON that Claude will read as a message echo "{\"type\": \"text\", \"text\": \"ESLint found errors in $FILE:\\n$RESULT\"}" exit 0 # exit 0 = allow, but Claude sees the message fi fi exit 0
The 3 Hook Types
Most people only know shell scripts. Two more types exist and they unlock patterns that shell scripts cannot reach.
Type A: Shell Script Hooks
A shell script that runs on the event. Receives tool input via environment variables. Returns exit code + optional JSON.
- Full access to the filesystem and CLI tools
- Can run any language (bash, python, node)
- Best for: file ops, linting, git checks, system commands
// settings.json { "hooks": { "PreToolUse": [ { "matcher": { "tool": "Bash" }, "hooks": [ { "type": "command", "command": "bash ~/.claude/hooks/bash-safety.sh" } ] } ] } }
Type B: HTTP Hooks
Instead of running a script, send an HTTP request to an endpoint. The response body becomes the hook output. Perfect for integrating with external systems.
- Trigger Slack notifications on specific events
- Update Notion pages when files change
- Log to analytics or monitoring dashboards
- Call webhooks on any platform
// settings.json { "hooks": { "PostToolUse": [ { "matcher": { "tool": "Bash", "command": "git push" }, "hooks": [ { "type": "http", "url": "https://hooks.slack.com/services/T00/B00/xxx", "method": "POST" } ] } ] } }
Type C: Prompt Hooks
Run an LLM on the hook event data. The prompt receives the tool input and returns a judgment. Use this for nuanced decisions that regex cannot handle.
- AI code reviewer before every bash execution
- Semantic safety checks on file writes
- Context-aware permission decisions
- Natural language policy enforcement
// settings.json { "hooks": { "PreToolUse": [ { "matcher": { "tool": "Bash" }, "hooks": [ { "type": "prompt", "prompt": "Is this bash command safe? If risky, respond EXIT_CODE=2. Command: {{input.command}}" } ] } ] } }
Async Hooks
Add "async": true and the hook runs in the background. Claude does not wait for it. Use for fire-and-forget operations.
When to use async
Logging to external services, analytics tracking, sending notifications, syncing state. Anything where you do not need the result to decide whether to proceed.
// settings.json — async logging hook { "hooks": { "PostToolUse": [ { "matcher": {}, "hooks": [ { "type": "command", "command": "bash ~/.claude/hooks/log-changes.sh", "async": true } ] } ] } }
#!/bin/bash # log-changes.sh — runs in background after every tool use DATE=$(date +"%Y-%m-%d %H:%M:%S") TOOL="$CLAUDE_TOOL_NAME" FILE="$CLAUDE_TOOL_INPUT_FILE_PATH" echo "[$DATE] $TOOL on $FILE" >> ~/.claude/changes-$(date +%Y-%m-%d).log
Matcher Patterns
Matchers filter which tool calls trigger your hook. Click any pattern below to highlight it.
Matcher Builder
Select a tool and optional filter to generate the matcher JSON.
{}The 10 Most Powerful Hook Patterns
Production-ready patterns. Each one is a full implementation you can copy and use immediately.
Hook Builder
Configure your hook and get ready-to-paste code. Select your options, generate the settings.json snippet and the shell script.
{
"hooks": {
"PreToolUse": [
{
"matcher": {},
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/my-hook.sh"
}
]
}
]
}
}#!/bin/bash # ~/.claude/hooks/my-hook.sh # Add your logic here echo "Hook fired on PreToolUse" exit 0
Knowledge Check
5 questions. Test what you just learned.
Q1: What exit code blocks a tool call?
Q2: What makes async hooks different from regular hooks?
Q3: What is a "prompt hook" used for?
Q4: If a PostToolUse hook returns JSON with a message, what happens?
Q5: Which hook event fires when Claude finishes a response?