What Claude Code Hooks Actually Are
Claude Code hooks are shell commands that run automatically when specific events happen during a session. Unlike git hooks (which only fire on git events), Claude Code hooks cover the entire session lifecycle - from the moment a session starts to every tool call, compaction event, subagent spawn, and session end.
I used to run a mental checklist before every commit: check for secrets, verify tests pass, monitor context usage. It was exhausting, and I forgot steps when deep in a problem. Six months ago I wrote my first hook - a security checker that blocks commands containing API keys. It saved me on day one. Now I run over 20 hooks across my system, and I haven't manually checked a single thing in months.
The breaking point was pushing credentials to a private repo. Nothing catastrophic, but enough to make me automate everything I was relying on memory for. If I catch myself repeating a check three times, it becomes a hook.
TL;DR: Claude Code has 25 hook event types. Three can block actions (PreToolUse, PermissionRequest, ConfigChange). Hooks are shell commands configured in settings.json that receive JSON on stdin and return JSON on stdout. Start with one high-value hook, then compose.
All 25 Hook Event Types
Claude Code fires hooks at 25 distinct points in the session lifecycle. Understanding the full surface area is critical for designing useful automations.
Session Lifecycle
| Event | When It Fires | Can Block? |
|---|---|---|
| SessionStart | Session begins or resumes | No |
| SessionEnd | Session terminates | No |
| Stop | Claude finishes responding | No |
| StopFailure | API error terminates a response | No |
User Input
| Event | When It Fires | Can Block? |
|---|---|---|
| UserPromptSubmit | User submits a prompt, before processing | No |
UserPromptSubmit is the most underrated hook. It runs before Claude sees your message, so you can inject context, calculate scores, or transform the prompt. My delegation enforcer uses this to score every prompt and inject routing hints.
Tool Execution
| Event | When It Fires | Can Block? |
|---|---|---|
| PreToolUse | Before a tool executes | Yes |
| PostToolUse | After a tool succeeds | No |
| PostToolUseFailure | After a tool fails | No |
| PermissionRequest | Permission dialog appears | Yes |
PreToolUse is the gatekeeper. When it returns "decision": "deny", the tool call is blocked entirely. This is where security checks, file protection rules, and command validation live.
Subagents and Tasks
| Event | When It Fires | Can Block? |
|---|---|---|
| SubagentStart | Subagent spawns | No |
| SubagentStop | Subagent finishes | No |
| TaskCreated | New task created | No |
| TaskCompleted | Task marked complete | No |
| TeammateIdle | Teammate agent about to go idle | No |
Context and Configuration
| Event | When It Fires | Can Block? |
|---|---|---|
| PreCompact | Before context compaction | No |
| PostCompact | After compaction completes | No |
| InstructionsLoaded | CLAUDE.md or rules file loaded | No |
| ConfigChange | Configuration file changes | Yes |
| CwdChanged | Working directory changes | No |
| FileChanged | Watched file changes on disk | No |
Workspace
| Event | When It Fires | Can Block? |
|---|---|---|
| WorktreeCreate | Git worktree created | No |
| WorktreeRemove | Git worktree removed | No |
| Notification | System notification sent | No |
| Elicitation | MCP server requests user input | No |
| ElicitationResult | User responds to MCP elicitation | No |
The Three Blocking Events
Only three events can actually prevent something from happening:
- PreToolUse - block a tool call before it executes
- PermissionRequest - auto-approve or auto-deny a permission dialog
- ConfigChange - reject a configuration change
Every other event is observe-only. Your hook runs, its output enters context as additionalContext, but the action proceeds regardless. This is an important design constraint - most hooks inform rather than control.
This lives in primeline-ai/evolving-lite - the self-evolving Claude Code plugin. Free, MIT, no build step.
How Hook Configuration Works
Hooks live in your settings.json file. The structure maps event names to arrays of matcher/command pairs:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/security-tier-check.py"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/delegation-enforcer.py"
}
]
}
]
}
}
Matcher Patterns
The matcher field filters when a hook fires:
- Empty string
""- matches every event of that type - Tool name
"Bash"- matches only when that specific tool is used - Pipe-separated
"Edit|Write|MultiEdit"- matches any of the listed tools - Glob patterns - match file paths or other event data
For PreToolUse and PostToolUse, the matcher tests against the tool name. An empty matcher catches all tool calls of that event type.
Where Settings Live
Settings files follow the same scoping as CLAUDE.md:
| Scope | File | Use Case |
|---|---|---|
| User | ~/.claude/settings.json | Hooks you want in every project |
| Project | .claude/settings.json | Hooks specific to this project (gitignored) |
| Project (shared) | .claude/settings.json (committed) | Hooks shared with team |
Multiple hooks on the same event run in parallel. Results are collected and all additionalContext strings are concatenated into Claude's context.
The Hook Response Protocol
Every hook receives JSON on stdin and must return JSON on stdout. The input always includes session_id and hook_event_name. Tool-related events add tool_name and tool_input.
Input Example (PreToolUse)
{
"session_id": "abc-123",
"cwd": "/Users/dev/my-project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf ./data"
}
}
Output Schema
{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "Text injected into Claude's context"
}
}
The decision field accepts three values:
| Decision | Effect | Available On |
|---|---|---|
"allow" | Action proceeds normally | All blocking events |
"deny" | Action is blocked | PreToolUse, PermissionRequest, ConfigChange |
"ask" | Prompts user for confirmation | PreToolUse, PermissionRequest |
The additionalContext string appears in Claude's context window as a system reminder. This is how hooks communicate with Claude - they inject information, warnings, or instructions that Claude sees on the next turn.
Hook 1: Security Tier Check (PreToolUse)
This hook blocks dangerous commands on production branches. It checks the command being executed against a severity tier and the current git branch.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/security-tier-check.py"
}
]
}
]
}
}
#!/usr/bin/env python3
import json, sys, subprocess
DANGEROUS_PATTERNS = {
"rm -rf": 8,
"drop database": 9,
"git push --force": 7,
"git reset --hard": 7,
"chmod 777": 6,
"curl | bash": 9,
}
PROTECTED_BRANCHES = ["main", "production", "release"]
def get_branch():
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True, text=True, timeout=5
)
return result.stdout.strip()
except Exception:
return "unknown"
data = json.loads(sys.stdin.read())
command = data.get("tool_input", {}).get("command", "")
branch = get_branch()
max_tier = 0
matched = ""
for pattern, tier in DANGEROUS_PATTERNS.items():
if pattern in command.lower():
if tier > max_tier:
max_tier = tier
matched = pattern
if max_tier >= 7 and branch in PROTECTED_BRANCHES:
print(json.dumps({
"decision": "deny",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": f"BLOCKED: '{matched}' (tier {max_tier}) on protected branch '{branch}'. Switch to a feature branch or remove the dangerous pattern."
}
}))
elif max_tier >= 5:
print(json.dumps({
"decision": "ask",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": f"WARNING: '{matched}' (tier {max_tier}) detected. Proceed with caution."
}
}))
else:
print(json.dumps({"decision": "allow"}))
Why Tiers Instead of Block-Everything
My first version blocked all destructive commands everywhere. That lasted two hours before I turned it off - too annoying on feature branches where rm -rf node_modules is routine. The tier system means risk scales with context. Tier 5-6 on a feature branch? Warning. Tier 7+ on main? Hard block.
Since adding this hook, I have been blocked from dangerous operations 23 times. Every single one was a mistake I would have regretted.
Hook 2: Delegation Enforcer (UserPromptSubmit)
This hook scores every prompt for delegation potential and injects routing hints into context. It runs before Claude processes the message, so Claude sees the delegation score as part of the prompt context.
#!/usr/bin/env python3
import json, sys
THRESHOLD = 3
SCORE_FACTORS = {
"explore": 3, "search": 3, "find": 3, "investigate": 3,
"review": 2, "refactor": 2, "research": 2, "audit": 2,
"debug": 2, "test": 2, "bulk": 2,
}
DEDUCTIONS = {
"production": -10, "deploy": -10, "password": -10,
"secret": -10, "credential": -10,
}
def score_prompt(prompt: str) -> int:
score = 0
lower = prompt.lower()
for keyword, points in SCORE_FACTORS.items():
if keyword in lower:
score += points
for keyword, points in DEDUCTIONS.items():
if keyword in lower:
score += points
return score
data = json.loads(sys.stdin.read())
prompt = data.get("prompt", "")
score = score_prompt(prompt)
if score >= THRESHOLD:
output = {
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": (
f"Delegation score: {score}. "
f"Consider delegating this task to a subagent."
)
}
}
else:
output = {"decision": "allow"}
print(json.dumps(output))
What the Production Version Adds
The simplified version above shows the core pattern. My production version in Evolving Lite adds:
- Model routing: Complexity 1-2 sends to Haiku, 3-6 to Sonnet, 7+ stays on Opus
- Trait injection: Loads personality profiles (curious for research, cautious for debugging) from config
- Coordination awareness: Reads active agent intents to avoid duplicate work across parallel sessions
- Inline hints: Tags like
[explore]or[debug]that Claude picks up for behavior adjustment
The delegation scoring system explains the full factor table and model routing logic.
Hook 3: Context Warning (PostToolUse)
When Claude reads large files or produces long outputs, context fills up fast. This hook tracks approximate token usage and warns at thresholds.
#!/bin/bash
# context-warning.sh - PostToolUse hook
# Reads tool output size from stdin, maintains running count
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('tool_name',''))")
# Track cumulative context growth in a temp file
TRACKER="/tmp/claude-context-$$"
[ -f "$TRACKER" ] || echo "0" > "$TRACKER"
CURRENT=$(cat "$TRACKER")
# Estimate tokens from tool result (rough: 1 token per 4 chars)
TOOL_OUTPUT_SIZE=$(echo "$INPUT" | wc -c)
ESTIMATED_TOKENS=$((TOOL_OUTPUT_SIZE / 4))
NEW_TOTAL=$((CURRENT + ESTIMATED_TOKENS))
echo "$NEW_TOTAL" > "$TRACKER"
# Threshold warnings
if [ "$NEW_TOTAL" -gt 800000 ]; then
echo "{\"decision\":\"allow\",\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"CRITICAL: Context estimated at ${NEW_TOTAL} tokens (80%+). Stop loading new files. Prepare session handoff or run /compact.\"}}"
elif [ "$NEW_TOTAL" -gt 600000 ]; then
echo "{\"decision\":\"allow\",\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"WARNING: Context estimated at ${NEW_TOTAL} tokens (60%+). Prefer subagent delegation for remaining research. Load summaries, not full docs.\"}}"
else
echo "{\"decision\":\"allow\"}"
fi
Before this hook existed, I regularly hit context limits mid-task and lost work. Now I get early warnings and can plan accordingly. My sessions stay lean because the hook forces cleanup as I go.
Why PostToolUse, Not PreToolUse?
A PreToolUse context warning would block the tool call. But at that point, you have already committed to the action - blocking it just causes confusion. PostToolUse warns after the fact, giving Claude information to adjust its next decision. The hook informs; Claude decides.
Hook 4: Auto-Format on Edit (PostToolUse)
The most common use case from the official docs: automatically format files after Claude edits them.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/auto-format.sh"
}
]
}
]
}
}
#!/bin/bash
# auto-format.sh - runs formatter on edited files
INPUT=$(cat)
FILE=$(echo "$INPUT" | python3 -c "
import json, sys
data = json.load(sys.stdin)
ti = data.get('tool_input', {})
print(ti.get('file_path', ti.get('path', '')))
")
if [ -z "$FILE" ]; then
echo '{"decision":"allow"}'
exit 0
fi
EXT="${FILE##*.}"
case "$EXT" in
ts|tsx|js|jsx)
npx prettier --write "$FILE" 2>/dev/null
;;
py)
python3 -m black --quiet "$FILE" 2>/dev/null
;;
rs)
rustfmt "$FILE" 2>/dev/null
;;
esac
echo '{"decision":"allow"}'
This hook fires on every Edit, Write, or MultiEdit tool call. The matcher "Edit|Write|MultiEdit" catches all three. The script extracts the file path from the tool input, detects the language from the extension, and runs the appropriate formatter.
The result: Claude's edits are always formatted according to your project standards, with zero manual intervention.
Hook 5: Pre-Compact State Save (PreCompact)
When compaction runs, conversation history gets summarized and path-scoped rules are lost. This hook saves critical session state before that happens.
#!/usr/bin/env python3
import json, sys, os
from datetime import datetime
data = json.loads(sys.stdin.read())
session_id = data.get("session_id", "unknown")
cwd = data.get("cwd", os.getcwd())
# Save session state snapshot
state = {
"session_id": session_id,
"timestamp": datetime.now().isoformat(),
"cwd": cwd,
"event": "pre_compact"
}
state_dir = os.path.expanduser("~/.claude/state")
os.makedirs(state_dir, exist_ok=True)
state_file = os.path.join(state_dir, f"pre-compact-{session_id[:8]}.json")
with open(state_file, "w") as f:
json.dump(state, f, indent=2)
print(json.dumps({
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "PreCompact",
"additionalContext": f"Pre-compact state saved to {state_file}. Path-scoped rules will be lost after compaction - re-read relevant files to reload them."
}
}))
Pairing PreCompact with PostCompact
PreCompact saves state. PostCompact restores it. Together they create a compaction-resilient workflow:
- PreCompact - extract decisions, task state, key file paths
- Compaction runs - conversation history summarized
- PostCompact - re-inject the saved state as
additionalContext
This is the hook-based version of the compact-stuff strategy for preserving session knowledge through compaction events.
- -Check git branch before dangerous commands
- -Estimate task complexity for delegation
- -Monitor context window usage
- -Run formatter after every edit
- -Remember to save state before compaction
- -Validate no sensitive data staged
- +security-tier-check blocks by branch + tier
- +delegation-enforcer scores and routes automatically
- +context-warning alerts at 60% and 80%
- +auto-format runs Prettier/Black on every edit
- +pre-compact saves session state snapshot
- +All hooks: 0.8s combined execution time
Designing Hooks That Compose
Individual hooks solve individual problems. The real power comes from composition - hooks that work together without explicitly knowing about each other.
The Composition Pattern
Each hook follows the same contract: JSON in, JSON out, additionalContext for communication. This means hooks compose naturally through context injection:
A single user prompt triggers this cascade:
- UserPromptSubmit - delegation score calculated, routing hint injected
- Claude processes the prompt, decides to call a tool
- PreToolUse - security check validates the tool call
- Tool executes
- PostToolUse - formatter runs, context warning updates, tracker logs
No hook knows about the others. They all communicate through additionalContext injections that Claude reads naturally.
Performance Budget
Hooks run as shell commands - they add latency. My rule: each hook gets a 200ms budget. If a hook needs more than that, it spawns a background process and returns immediately. Across all my hooks, average combined execution time is 0.8 seconds per session turn.
Keep hooks fast by:
- Avoid network calls in synchronous hooks (use background for API calls)
- Cache expensive computations (git branch doesn't change mid-command)
- Return early when the matcher already filtered to relevant events
Production Results After 6 Months
After six months running 20+ production hooks across multiple projects:
- Manual checks per commit: 6 - 0
- Context limit hits: 12/month - 1/month
- Accidental dangerous commands: 4 - 0
- Delegation decisions: Manual - Automatic (83% of tasks)
- Average hook execution time: 0.8 seconds combined
- Sessions lost to context overflow: 8 - 0
The biggest surprise was not time saved - it was consistency. Hooks do not forget. They do not skip steps when you are tired. That reliability compounds across hundreds of sessions.
Getting Started With Your First Hook
You do not need 20 hooks on day one. Start with one that solves a real pain point:
- If you hit context limits - start with a PostToolUse context warning
- If you have pushed secrets - start with a PreToolUse security check
- If you handle too much manually - start with a UserPromptSubmit delegation enforcer
- If your code style drifts - start with a PostToolUse auto-formatter
The Minimal Hook Template
Every hook follows the same pattern. Copy this and customize:
#!/usr/bin/env python3
import json, sys
data = json.loads(sys.stdin.read())
# Your logic here
print(json.dumps({
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": data.get("hook_event_name", ""),
"additionalContext": "Your message to Claude here"
}
}))
Add it to your settings.json, pick the right event and matcher, and you have a working hook. The Evolving Lite plugin includes 10 production hooks ready to install - security, delegation, context management, and more.
Run /hooks in Claude Code to inspect your current hook configuration and verify everything is wired up correctly.
![Claude Code Hooks: Automate Your Workflow [2026]](/_next/image?url=%2Fblog%2Fhooks-automation-hero.webp&w=3840&q=75)

![Claude Code Persistent Memory: Setup Guide [2026]](/_next/image?url=%2Fblog%2Fmemory-system-hero.webp&w=3840&q=75)
