Claude Code Hooks: Why Each of My 95 Hooks Exists
I built 95 hooks for Claude Code. Every one exists because something went wrong first. The git-safety-guardian exists because Claude force-pushed to main. The recursion-guard exists because a subagent spawned infinite children. The blog-quality-gate exists because I published a post with 7 passive voice sentences and a dangling footnote.1
TL;DR
Claude Code hooks execute shell commands at specific lifecycle points during AI-assisted development. Hooks provide deterministic guarantees (blocking dangerous git commands, injecting context, enforcing quality) on top of a probabilistic system (the LLM). After building 95 hooks across my infrastructure, I’ve found that the best hooks come from incidents, not planning. This post covers the architecture, the origin stories behind my most critical hooks, and the patterns I’ve learned from 9 months of hook development.
The Architecture
Claude Code exposes lifecycle events where hooks can intercept, modify, or block behavior:2
Session Events
| Event | When It Fires | My Hooks |
|---|---|---|
| SessionStart | New session begins | session-start.sh — injects date, validates venv, initializes recursion state |
| SessionEnd | Session terminates | Clean up temp files |
Tool Execution Events
| Event | When It Fires | My Hooks |
|---|---|---|
| PreToolUse | Before any tool executes | git-safety-guardian.sh, recursion-guard.sh, credentials-check.sh |
| PostToolUse | After tool completes | post-deliberation.sh, log-bash.sh |
Response Events
| Event | When It Fires | My Hooks |
|---|---|---|
| UserPromptSubmit | User sends a prompt | Context injectors (date, branch, model info) |
| Stop | Claude finishes responding | deliberation-pride-check.sh, reviewer-stop-gate.sh |
Every hook receives JSON on stdin and communicates through stdout:
{"decision": "block", "reason": "Force push to main is prohibited"}
Or silently allows by exiting with code 0.3
Origin Stories: The Hooks That Matter Most
Hook 1: git-safety-guardian.sh (PreToolUse:Bash)
The incident: During an early Claude Code session, I asked the agent to “clean up the git history.” The agent ran git push --force origin main. The force push overwrote three days of commits on a shared branch. I recovered from a local backup, but the 4-hour recovery process convinced me that probabilistic judgment should never control destructive git operations.
The hook:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Only check git commands
echo "$COMMAND" | grep -qE '\bgit\b' || exit 0
# Block force push to main/master
if echo "$COMMAND" | grep -qiE 'git\s+push\s+.*--force.*\b(main|master)\b'; then
cat << EOF
{"decision": "block", "reason": "Force push to main/master blocked by safety hook"}
EOF
fi
The lesson: The hook doesn’t try to understand intent. It pattern-matches on the command string. Simple, deterministic, impossible to bypass through clever prompting. The agent can still force-push to feature branches (sometimes legitimate), but main/master are permanently protected.4
Lifetime saves: 8 intercepted force-push attempts across 9 months.
Hook 2: recursion-guard.sh (PreToolUse:Task)
The incident: While building the deliberation system, I ran a session that spawned 3 exploration subagents. Each subagent, lacking spawn limits, spawned its own subagents. The recursion consumed API tokens at 10x the normal rate. I killed the session manually after noticing the accelerating token burn.
The hook:
#!/bin/bash
CONFIG_FILE="${HOME}/.claude/configs/recursion-limits.json"
STATE_FILE="${HOME}/.claude/state/recursion-depth.json"
MAX_DEPTH=2
MAX_CHILDREN=5
DELIB_SPAWN_BUDGET=2
DELIB_MAX_AGENTS=12
# Validate integers safely (((VAR++)) crashes with set -e when VAR=0)
is_positive_int() {
[[ "$1" =~ ^[0-9]+$ ]] && [[ "$1" -gt 0 ]]
}
The key design decision: Agents inherit a spawn budget from their parent rather than incrementing depth. A root agent with budget=12 can distribute that budget across any tree shape. Depth-based limits are too rigid (they prevent deep-but-narrow chains that are perfectly safe).5
Lifetime blocks: 23 runaway spawn attempts.
Hook 3: blog-quality-gate.sh (Stop)
The incident: I published a blog post with 7 passive voice sentences, a footnote referenced in the text but missing from the references section, and “was implemented by the team” as the opening line. The post looked polished in my editor but failed basic quality checks that any human reviewer would catch.
The hook: Runs my 12-module blog linter on any modified blog content file. Checks for passive voice, dangling footnotes, missing meta descriptions, untagged code blocks, and citation integrity. Each finding is specific: “Line 47: passive voice detected in ‘was implemented by the team.’ Suggestion: ‘the team implemented.’”
The parallel with human feedback: The hook critiques the work, not the operator. It says “line 47 has passive voice,” not “you write poorly.” The same principle that makes human feedback constructive makes automated feedback useful.
The Pattern Behind 95 Hooks
The Config-Driven Architecture
My hooks evolved from hardcoded values to config-driven behavior:
~/.claude/configs/
├── recursion-limits.json # Depth, spawn budgets, timeouts
├── deliberation-config.json # Consensus thresholds per task type
├── consensus-profiles.json # security=85%, docs=50%
├── circuit-breaker.json # Failure mode configurations
└── file-scope-rules.json # Path-scoped hook application
Moving thresholds to JSON configs meant I could tune behavior without editing bash scripts. When I needed security-related consensus at 85% but documentation at 50%, the config change took 30 seconds. A code change would have required editing, testing, and redeploying.6
The Lifecycle Layering Pattern
My 95 hooks form a safety net with four layers:
Layer 1: Prevention (PreToolUse) — Stop bad things before they happen. git-safety-guardian, credentials-check, recursion-guard.
Layer 2: Context (UserPromptSubmit, SessionStart) — Inject information the agent needs. Date, branch, project context, memory entries, philosophy principles.
Layer 3: Validation (PostToolUse) — Verify that completed actions meet standards. post-deliberation consensus checking, output logging.
Layer 4: Quality (Stop) — Gate the final output. Pride check, quality gate, reviewer stop gate. This layer implements metacognitive monitoring — the agent evaluates its own reasoning quality, not just its output.
Each layer is independent. If a PreToolUse hook fails silently, the Stop hook still catches quality issues. Defense in depth, applied to AI agent behavior.
Configuration
Hooks live in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "~/.claude/hooks/git-safety-guardian.sh",
"timeout": 5000
}
],
"PreToolUse:Task": [
{
"command": "~/.claude/hooks/recursion-guard.sh"
}
]
}
}
The matcher field filters which tools trigger the hook. PreToolUse:Task is shorthand for a matcher that only fires on Task tool invocations. Async hooks (async: true) run in the background without blocking.7
Scope Hierarchy
- User-level (
~/.claude/settings.json) — applies to all projects. My 95 hooks live here. - Project-level (
.claude/settings.json) — adds project-specific hooks. - Skill/Subagent frontmatter — scoped to a specific component’s lifecycle.8
What I’d Do Differently
Start with 3 hooks, not 25. My first month produced 25 hooks, many of which added context that the agent already had. The overhead of loading 25 hooks on every tool call was measurable. I eventually pruned to the hooks that produced real value (prevented real incidents, caught real quality issues).
Config-driven from day one. I spent two weeks refactoring hardcoded thresholds into JSON configs. That refactoring would have been free if I’d started with configs.
Test infrastructure early. The first 20 hooks had no tests. When I added the test harness (48 bash integration tests), I found 3 hooks that silently failed on edge cases. Tests should have shipped with hook #1.
Key Takeaways
For developers starting with hooks: - Start with three hooks: git safety (PreToolUse:Bash), context injection (UserPromptSubmit), and quality gate (Stop); add more only when you have incidents that justify them - Use the decision timing framework: hook architecture is irreversible (95 hooks depend on it), so invest in the lifecycle model before writing hooks
For teams standardizing hooks: - Standardize hooks at the user level so every team member gets the same safety rails - Track hook metrics (blocked operations, intercepted incidents) to justify the maintenance cost - Review hook logs monthly to identify new patterns worth automating
References
-
Author’s hook infrastructure. 95 hooks across 6 lifecycle events, developed over 9 months (2025-2026). ↩
-
Anthropic, “Claude Code Documentation,” 2025. Hook lifecycle events. ↩
-
Anthropic, “Claude Code Documentation,” 2025. Hook input/output JSON format. ↩
-
Author’s git-safety-guardian.sh. 8 intercepted force-push attempts tracked in
~/.claude/state/. ↩ -
Author’s recursion-guard.sh. Budget inheritance model documented in
~/.claude/configs/recursion-limits.json. ↩ -
Author’s config-driven architecture. 14 JSON config files encoding all hook thresholds and rules. ↩
-
Anthropic, “Claude Code Documentation,” 2025. Hook configuration and async execution. ↩
-
Anthropic, “Claude Code Documentation,” 2025. Hook scope hierarchy. ↩