Claude Code Hooks: Why Each of My 95 Hooks Exists

From the guide: Claude Code Comprehensive Guide

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

Claude Code hooks are shell commands that execute at specific lifecycle points (session start, before/after tool use, prompt submission, and response completion), providing deterministic guardrails on top of probabilistic LLM behavior. Hooks receive JSON on stdin and return decisions (block, allow, or modify) via stdout. They enforce policies like blocking force-pushes to main, preventing recursive agent spawns, injecting context per session, and gating output quality that the LLM alone cannot guarantee.

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 26 lifecycle events where hooks can intercept, modify, or block behavior (as of v2.1.116, April 2026).2 My hooks target six of the most common ones:

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 where 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

  1. User-level (~/.claude/settings.json): applies to all projects. My 95 hooks live here.
  2. Project-level (.claude/settings.json): adds project-specific hooks.
  3. 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


FAQ

What are Claude Code hooks?

Hooks are shell scripts that execute at specific lifecycle points during Claude Code sessions. They fire deterministically at specific moments: before a tool runs (PreToolUse), after it completes (PostToolUse), when you send a prompt (UserPromptSubmit), when a session starts (SessionStart), and when Claude finishes responding (Stop). Hooks receive JSON on stdin with full context (tool name, input, session ID) and can block operations, inject context, or enforce quality gates. They provide deterministic guarantees on top of a probabilistic LLM.

How many hooks can Claude Code run and is there a performance cost?

There is no hard limit on hook count. I run 95 hooks across 6 of the 26 available lifecycle events (as of v2.1.116, April 2026), with approximately 200ms total overhead per event. The practical limit is latency: each hook adds execution time before or after tool calls. The key optimization is using dispatchers (one per event, running hooks sequentially from cached stdin) rather than independent hooks that each read stdin separately. Concurrent writes from independent hooks caused JSON corruption in my early setup; dispatchers eliminated that failure mode entirely.

What events do Claude Code hooks fire on?

Claude Code exposes 26 lifecycle events for hooks as of v2.1.116 (April 2026). The six most common ones my hooks target are: SessionStart (new session begins), SessionEnd (session terminates), PreToolUse (before any tool executes), PostToolUse (after tool completes), UserPromptSubmit (user sends a prompt), and Stop (Claude finishes responding). Others include SubagentStart, PermissionRequest, PermissionDenied, TaskCreated, CwdChanged, FileChanged, PreCompact, and more. PreToolUse and PostToolUse can be further scoped with matchers like PreToolUse:Bash or PreToolUse:Task to fire only on specific tool types. Each event receives JSON on stdin with relevant context.

Can Claude Code hooks block tool calls?

Yes. A hook that outputs {"decision": "block", "reason": "explanation"} to stdout prevents the tool call from executing. This is the core mechanism for safety hooks: my git-safety-guardian blocks force pushes to main, my credentials-check blocks reads of .env files, and my recursion-guard blocks excessive agent spawning. The block is deterministic and cannot be bypassed through prompting. Hooks that exit silently with code 0 allow the operation to proceed.

What is the difference between PreToolUse and PostToolUse hooks?

PreToolUse fires before a tool executes and can block the operation entirely. Use it for safety gates: blocking dangerous commands, checking credentials, enforcing spawn budgets. PostToolUse fires after a tool completes and can provide feedback or trigger follow-up actions. Use it for validation: checking output quality, logging activity, verifying consensus. Together they form the first and third layers of a four-layer safety net: prevention (PreToolUse), context injection (UserPromptSubmit), validation (PostToolUse), and quality gating (Stop).

References


  1. Author’s hook infrastructure. 95 hooks across 6 of the 26 available lifecycle events (v2.1.116, April 2026), developed over 9 months (2025-2026). 

  2. Anthropic, “Claude Code Hooks Reference,” 2026. 26 lifecycle event types as of v2.1.116 (April 2026). 

  3. Anthropic, “Claude Code Documentation,” 2025. Hook input/output JSON format. 

  4. Author’s git-safety-guardian.sh. 8 intercepted force-push attempts tracked in ~/.claude/state/

  5. Author’s recursion-guard.sh. Budget inheritance model documented in ~/.claude/configs/recursion-limits.json

  6. Author’s config-driven architecture. 14 JSON config files encoding all hook thresholds and rules. 

  7. Anthropic, “Claude Code Documentation,” 2025. Hook configuration and async execution. 

  8. Anthropic, “Claude Code Documentation,” 2025. Hook scope hierarchy. 

Related Posts

Claude Code Skills: Build Custom Auto-Activating Extensions

Build custom Claude Code skills that auto-activate based on context. Step-by-step tutorial covering SKILL.md structure, …

13 min read

Context Window Management: 50 Sessions of Data

I measured token consumption across 50 Claude Code sessions. Context exhaustion degrades output before you notice. Here …

9 min read

Claude Code Hooks Tutorial: 5 Production Hooks From Scratch

Build 5 production Claude Code hooks from scratch with full JSON configs: auto-formatting, security gates, test runners,…

13 min read