>_

Claude Code Hooks: Automate Your Workflow [2026]

Robin||12 min
Last updated: April 13, 2026
claude-codehooksautomationproductivitytutorial
Claude Code Hooks: Automate Your Workflow [2026]

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

EventWhen It FiresCan Block?
SessionStartSession begins or resumesNo
SessionEndSession terminatesNo
StopClaude finishes respondingNo
StopFailureAPI error terminates a responseNo

User Input

EventWhen It FiresCan Block?
UserPromptSubmitUser submits a prompt, before processingNo

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

EventWhen It FiresCan Block?
PreToolUseBefore a tool executesYes
PostToolUseAfter a tool succeedsNo
PostToolUseFailureAfter a tool failsNo
PermissionRequestPermission dialog appearsYes

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

EventWhen It FiresCan Block?
SubagentStartSubagent spawnsNo
SubagentStopSubagent finishesNo
TaskCreatedNew task createdNo
TaskCompletedTask marked completeNo
TeammateIdleTeammate agent about to go idleNo

Context and Configuration

EventWhen It FiresCan Block?
PreCompactBefore context compactionNo
PostCompactAfter compaction completesNo
InstructionsLoadedCLAUDE.md or rules file loadedNo
ConfigChangeConfiguration file changesYes
CwdChangedWorking directory changesNo
FileChangedWatched file changes on diskNo

Workspace

EventWhen It FiresCan Block?
WorktreeCreateGit worktree createdNo
WorktreeRemoveGit worktree removedNo
NotificationSystem notification sentNo
ElicitationMCP server requests user inputNo
ElicitationResultUser responds to MCP elicitationNo

The Three Blocking Events

Only three events can actually prevent something from happening:

  1. PreToolUse - block a tool call before it executes
  2. PermissionRequest - auto-approve or auto-deny a permission dialog
  3. 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:

code
{
  "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:

ScopeFileUse Case
User~/.claude/settings.jsonHooks you want in every project
Project.claude/settings.jsonHooks 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)

code
{
  "session_id": "abc-123",
  "cwd": "/Users/dev/my-project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf ./data"
  }
}

Output Schema

code
{
  "decision": "allow",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "additionalContext": "Text injected into Claude's context"
  }
}

The decision field accepts three values:

DecisionEffectAvailable On
"allow"Action proceeds normallyAll blocking events
"deny"Action is blockedPreToolUse, PermissionRequest, ConfigChange
"ask"Prompts user for confirmationPreToolUse, 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.

code
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/security-tier-check.py"
          }
        ]
      }
    ]
  }
}
code
#!/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.

Security Tier Check Flow
Bash command triggered
v
Parse command + check git branch
v
Score against danger tier patterns
v
Tier < 5: allow | 5-6: ask | 7+ on protected: deny

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.

code
#!/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.

code
#!/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.

code
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/auto-format.sh"
          }
        ]
      }
    ]
  }
}
code
#!/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.

code
#!/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:

  1. PreCompact - extract decisions, task state, key file paths
  2. Compaction runs - conversation history summarized
  3. 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.

Manual Workflow
  • -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
Hook-Automated
  • +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:

Hook Composition
PreCompact / PostCompactState preservation across compaction
PostToolUseAuto-format + context warning + post-tool tracker
PreToolUseSecurity tier check validates the action
UserPromptSubmitDelegation enforcer injects score + model routing

A single user prompt triggers this cascade:

  1. UserPromptSubmit - delegation score calculated, routing hint injected
  2. Claude processes the prompt, decides to call a tool
  3. PreToolUse - security check validates the tool call
  4. Tool executes
  5. 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
hook-cascade

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:

  1. If you hit context limits - start with a PostToolUse context warning
  2. If you have pushed secrets - start with a PreToolUse security check
  3. If you handle too much manually - start with a UserPromptSubmit delegation enforcer
  4. 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:

code
#!/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.

FAQ

How many hook event types does Claude Code have?+
Claude Code has 25 hook event types covering the full session lifecycle: session start/end, user prompt submission, tool execution (pre/post/failure), subagent lifecycle, task events, context compaction, configuration changes, file watching, worktree management, notifications, and MCP elicitations.
Which Claude Code hooks can block actions?+
Only three hook events can block: PreToolUse (blocks a tool call before execution), PermissionRequest (auto-approves or auto-denies permission dialogs), and ConfigChange (rejects configuration changes). All other hooks are observe-only and inject context via additionalContext.
Do hooks slow down Claude Code?+
Well-designed hooks add minimal latency. Each hook should stay under 200ms. Multiple hooks on the same event run in parallel. Across 20+ production hooks, combined execution time averages 0.8 seconds per turn. Avoid network calls in synchronous hooks - use background processes for expensive operations.
What is the difference between Claude Code hooks and git hooks?+
Git hooks only trigger on git events like commit, push, or merge. Claude Code hooks trigger on 25 different events across the entire session lifecycle including tool calls, context compaction, subagent spawning, prompt submission, and configuration changes. They are far more flexible than git hooks.
How do Claude Code hooks communicate with Claude?+
Hooks return JSON with an additionalContext field. This text is injected into Claude's context window as a system reminder on the next turn. Claude reads it and adjusts behavior accordingly. This is the only communication channel - hooks cannot modify Claude's response directly.
Can I share hooks with my team?+
Yes. Hooks configured in .claude/settings.json at the project root can be committed to version control. Team members get the same hooks automatically. User-scoped hooks in ~/.claude/settings.json stay private. Project-scoped hooks require approval on first use for security.
What happens if a hook script crashes?+
If a hook script fails (non-zero exit, invalid JSON, timeout), Claude Code handles it gracefully. For blocking hooks like PreToolUse, the safest default is to deny the action on failure. For non-blocking hooks, the failure is logged but the session continues normally.
How do I test hooks before using them in production?+
Echo test JSON into your hook script: echo '{"session_id":"test","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | python3 .claude/hooks/security-tier-check.py. Verify the output JSON has the expected decision. Run /hooks in Claude Code to inspect your configuration.
What is the UserPromptSubmit hook used for?+
UserPromptSubmit fires before Claude processes a user message. Common uses include delegation scoring (calculating whether a task should be delegated to a subagent), prompt transformation, context injection, and routing hints. It cannot block the prompt but can inject additionalContext that Claude reads before responding.
How do PreCompact and PostCompact hooks work together?+
PreCompact fires before context compaction summarizes conversation history. Use it to save critical session state (decisions, task progress, key file paths). PostCompact fires after compaction completes. Use it to re-inject saved state as additionalContext so Claude recovers context that compaction would otherwise lose.

>_ Get the free Claude Code guide

>_ No spam. Unsubscribe anytime.

>_ Related