architecture
Advanced Hooks: Beyond the Basics
PreToolUse, PostToolUse, Stop, Notification: chain hooks for file protection, token tracking, auto-formatting, and audit logs.
Hooks run outside the LLM loop. No AI reasoning. No token cost. They intercept every tool call, session event, and notification before or after it happens. That gives you deterministic control over what Claude can and cannot do.
This breakdown covers 9 key lifecycle events (out of 30+ in total), the 3-code exit system, context injection, the 3 hook types (shell / HTTP / prompt), chaining patterns, and 5 production-ready examples you can copy immediately.
Section 01
Key Lifecycle Events
Every hook fires on a specific lifecycle event. PreToolUse, PostToolUse, and UserPromptSubmit are the workhorses; the rest handle session lifecycle. Most setups only need 3-4 events. (Claude Code exposes 30+ events total; these are the ones you will use most.)
| 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 |
| UserPromptSubmit | Before Claude sees the user message | Inject just-in-time context, append metadata, reject prompt on exit 2 |
| 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 |
PreToolUse and PostToolUse are the workhorses. The other 6 handle session lifecycle. Most setups only need 3-4 events total.
Section 02
The Exit Code System
Your hook's exit code tells Claude what to do next. Three possible outcomes, each with different behavior.
Proceed as planned. The tool call or event continues normally. If the hook outputs JSON, Claude receives it as context.
An error occurred in the hook but Claude continues the action anyway. Claude logs a notice with the first line of stderr. This does NOT block or prompt a retry. Only exit code 2 actually blocks.
Hard stop. Claude cannot proceed with this action. Your hook's stderr becomes the error message Claude sees.
#!/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
Section 03
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 0Key insight: exit 0 + JSON stdout = Claude proceeds AND receives your context. This is how you build feedback loops without blocking anything.
Section 04
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
The basics. Most people know this one.
A shell script that runs on the event. Receives tool input via environment variables. Returns exit code + optional JSON. Full access to the filesystem, can run any language, best for file ops, linting, git checks, and system commands.
// settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": { "tool": "Bash" },
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/bash-safety.sh"
}
]
}
]
}
}Type B: HTTP Hooks
Call external services. Almost nobody uses these.
Instead of running a script, send an HTTP request to an endpoint. The response body becomes the hook output. Perfect for triggering Slack notifications, updating Notion pages, calling webhooks, or logging to dashboards.
// settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool": "Bash", "command": "git push" },
"hooks": [
{
"type": "http",
"url": "https://hooks.slack.com/services/T00/B00/xxx",
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": "{\"text\": \"Code pushed to remote.\"}"
}
]
}
]
}
}Type C: Prompt Hooks
AI-powered safety checks. Almost nobody knows these exist.
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: semantic safety checks, context-aware permissions, natural language policy enforcement.
// settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": { "tool": "Bash" },
"hooks": [
{
"type": "prompt",
"prompt": "Is this bash command safe to run in a dev environment? If it could delete data, modify system files, or affect production, respond with EXIT_CODE=2 and explain why. Otherwise respond EXIT_CODE=0. Command: {{input.command}}"
}
]
}
]
}
}Section 05
Chaining Patterns
Multiple hooks can fire on the same event in sequence. The first blocking exit 2 stops the chain; subsequent hooks in the array do not run.
Guard → Log
A synchronous safety check runs first (can block). An async logging hook runs second, only if the action was allowed.
Lint → Notify
PostToolUse lints the edited file and returns JSON context. A second HTTP hook notifies the team channel.
Validate → Backup
PreToolUse validates the file path is allowed. A second hook snapshots the current file before the edit lands.
Scan → Audit
Secret scanner checks staged files before git commit. An audit log hook records the commit attempt regardless of outcome.
// settings.json: chained hooks on one event
{
"hooks": {
"PostToolUse": [{
"matcher": { "tool": "Edit", "path": "*config*" },
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/audit-log.sh"
},
{
"type": "http",
"url": "https://hooks.slack.com/services/T00/B00/xxx",
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": "{\"text\": \"Config file modified by Claude Code.\"}",
"async": true
}
]
}]
}
}Section 06
5 Production Examples
Ready-to-copy implementations. Click any pattern to expand the full code.
Section 07
settings.json Reference
The full settings.json structure for a production hook setup: async logging on every tool, file protection on writes, and cost tracking on stop.
{
"hooks": {
"PreToolUse": [
{
"matcher": { "tool": "Write" },
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/client-protect.sh"
}
]
},
{
"matcher": { "tool": "Bash", "command": "git commit" },
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/secret-scan.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": { "tool": "Edit" },
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/auto-lint.sh"
}
]
},
{
"matcher": {},
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/change-log.sh",
"async": true
}
]
}
],
"Stop": [
{
"matcher": {},
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/cost-tracker.sh",
"async": true
}
]
}
]
}
}Section 08
Debug Tips
Hooks fail silently by default. These patterns make them observable.
Log all env vars at hook start
Run `env | grep CLAUDE_` at the top of your hook during development to see exactly what variables are available for your event type.
Test exit codes in isolation
Write a minimal hook that only exits with a fixed code and verify Claude's behavior before adding real logic. exit 2 should halt the action every time.
stderr goes to Claude, stdout is data
Anything printed to stderr appears as the error message Claude sees on exit 2. Anything printed to stdout (as JSON) becomes context Claude can act on.
Use async: true for logging hooks
Any hook that only logs, notifies, or syncs state should be async. Synchronous hooks add latency to every tool call; keep them lean.
Chain hooks for layered enforcement
Place multiple hooks in the hooks array under one matcher. They run in order. A blocking exit 2 from the first stops the rest from running.
#!/bin/bash # Debug header: add to top of any hook during development echo "=== HOOK DEBUG ===" >&2 echo "Tool: $CLAUDE_TOOL_NAME" >&2 echo "File: $CLAUDE_TOOL_INPUT_FILE_PATH" >&2 echo "Command: $CLAUDE_TOOL_INPUT_COMMAND" >&2 echo "Project: $CLAUDE_PROJECT_DIR" >&2 echo "==================" >&2 # ...rest of your hook logic
For a hook that prevents secrets from reaching git, see our Claude Code security audit guide. For how hooks fit into the full context engineering system, see Context Engineering Mastery. Need production hook setup for your team? Book a consultation.
Want this running in your stack?
AY Automate builds agent systems, RAG pipelines, and Claude Code setups for production teams.