>_

Claude Code Hooks: Automate Your Workflow [2026]

Robin||12 min
Last updated: August 15, 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 31 hook event types and 18 of them can block. Hooks are shell commands configured in settings.json that receive JSON on stdin. They print JSON back only when they want to change something, and for almost every event exit 0 with no output is how a hook says carry on. WorktreeCreate is the exception, and an expensive one - see below. Start with one high-value hook, then compose.

All 31 Hook Event Types

Claude Code fires hooks at 31 distinct points in the session lifecycle. Understanding the full surface area is critical for designing useful automations.

Counted against the v2.1.233 binary on 2026-08-15, not from the docs summary table - the two disagree, and the callout below the tables says where and why. 18 of the 31 can stop something from happening.

Session Lifecycle

EventWhen It FiresCan Block?
SessionStartSession begins or resumesNo
SetupRepo setup, and only from the CLI: --init-only, --init, or --maintenance. Never on normal startup, and not from typing /init in a session. Receives a trigger of init or maintenanceNo
SessionEndSession terminatesNo
StopClaude finishes respondingYes
StopFailureAPI error terminates a responseNo

User Input

EventWhen It FiresCan Block?
UserPromptSubmitUser submits a prompt, before processingYes
UserPromptExpansionA slash command expands into its promptYes
MessageDisplayAssistant text streams to the screenNo

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 succeedsYes
PostToolUseFailureAfter a tool failsYes
PostToolBatchAfter a batch of parallel tool callsYes
PermissionRequestPermission dialog appearsYes
PermissionDeniedAuto-mode classifier denied a tool callNo

PreToolUse is the gatekeeper, and it is the one event with its own response shape. It blocks by returning hookSpecificOutput.permissionDecision set to "deny". A top-level "decision" field is deprecated for this event rather than inert - it is still honoured - so write the nested form in new hooks, but do not rewrite a working legacy hook on the assumption that it has stopped working. This is where security checks, file protection rules, and command validation live. The post-tool events can block too, but later: they cannot un-run the tool, they stop Claude from continuing after it.

Subagents and Tasks

EventWhen It FiresCan Block?
SubagentStartSubagent spawnsNo
SubagentStopSubagent finishesYes
TaskCreatedNew task createdYes
TaskCompletedTask marked completeYes
TeammateIdleTeammate agent about to go idleYes

Context and Configuration

EventWhen It FiresCan Block?
PreCompactBefore context compactionYes
PostCompactAfter compaction completesNo
InstructionsLoadedCLAUDE.md or rules file loadedNo
ConfigChangeConfiguration file changesYes
CwdChangedWorking directory changesNo
DirectoryAddedA directory is added to the workspaceNo
FileChangedWatched file changes on diskNo

Workspace

EventWhen It FiresCan Block?
WorktreeCreateGit worktree created, via --worktree, isolation: "worktree", or for a background sessionYes
WorktreeRemoveA hook-provided worktree is removed. Did not fire for one Claude Code created itself, in any attemptYes
NotificationSystem notification sentNo
ElicitationMCP server requests user inputYes
ElicitationResultUser responds to MCP elicitationYes

The 18 Blocking Events, and How They Block

Eighteen of the 31 can stop something from happening, and they do not all use the same field. This is the part that bites people, because a hook that returns the wrong shape fails silently: it exits 0, Claude Code reads no decision, and the action proceeds.

These are not three tidy buckets, and I have published a tidy-bucket version of this twice now and been wrong both times. The mechanisms overlap:

Exit code 2 is the general one. A hook that writes its reason to stderr and exits 2 blocks, for most of the 18. I tested this on UserPromptSubmit, which I had previously filed under "top-level decision only": the run comes back "num_turns": 0 with result: "UserPromptSubmit operation blocked by hook: ...". If you only learn one mechanism, learn this one.

A top-level "decision": "block" also works, for many of the same events - UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, PreCompact, and TaskCreated. Same isolated-config test, same result. So for a good number of events, either shape blocks, and the two lists are not complements.

Two events have their own nested shape, and here the shape genuinely matters: PreToolUse uses hookSpecificOutput.permissionDecision, PermissionRequest uses hookSpecificOutput.decision.behavior.

And a few take {"continue": false} - TaskCompleted and TeammateIdle among them.

WorktreeCreate is none of the above, and it is the one that will bite you. It is not a veto hook, it is a provider: its job is to return the path of the worktree it created, on stdout for a command hook or as hookSpecificOutput.worktreePath otherwise. Anything else aborts the operation. I fired three hooks at it against v2.1.233 and all three stopped the worktree being created - exit 1, exit 2, and exit 0 with no output:

code
Error creating worktree: WorktreeCreate hook failed: hook succeeded but
returned no worktree path (command: echo the path to stdout; ...)

So the general rule above - exit 0 and say nothing to let things proceed - is false here, and expensively so. Register an observe-only logger on WorktreeCreate and you break every worktree creation on the machine, including the ones Claude Code makes for background sessions, with an error that does not mention your hook. If you want to watch worktree creation without owning it, log from inside the worktree after the fact. Do not reach for WorktreeRemove as the observation point either: it cannot tell you a worktree was created, and it only fires for a worktree that a WorktreeCreate hook provided in the first place. That precondition is why it looks dead - three of my attempts had it never fire, because Claude Code had built the worktree itself.

The practical rule: write the event's documented shape, and exit 2 as well if you want belt and braces. What you must not do is assume that because one shape is documented for an event, the other is inert.

The other 13 are observe-only. Your hook runs, its output can enter context as additionalContext, but the action proceeds regardless. Most hooks inform rather than control, and that is still the right default.

Where this disagrees with the docs, and why

The hooks reference's own decision-control table implies 14, because it does not list Elicitation or ElicitationResult as blocking and it files ConfigChange under top-level decision. The v2.1.233 binary says otherwise, in its own words: Exit code 2 - deny the elicitation, Exit code 2 - block the response (action becomes decline), and for ConfigChange Exit code 2 - block the change from being applied to the session. I counted from the binary, because it is the thing that runs. I have now published 3, then 14, then 16 for this number, and each time I had counted rather than tested. WorktreeCreate is what broke 16: I had it marked observe-only until a reviewer refused a worktree with it.

Make that four wrong answers. I wrote a version of this paragraph claiming 17 and naming WorktreeRemove as the row I most distrusted, on the grounds that I could not get it to fire. A reviewer then fired it - and it blocks. The reason mine never fired is a precondition neither of us had thought to vary: WorktreeRemove only dispatches for a worktree that a WorktreeCreate hook provided. When Claude Code builds the worktree itself, the event never comes. Register a providing WorktreeCreate hook and its sibling starts firing, and an exit 2 leaves the worktree in place with could not remove it.

So here is the honest state of 18. Nine of the 31 events have actually had a hook fired at them. Three confirm blocking: UserPromptSubmit (via exit 2 and via a top-level decision), WorktreeCreate, WorktreeRemove. Six confirm the opposite - SessionStart, Setup, SessionEnd, MessageDisplay, InstructionsLoaded, SubagentStart - four of them tested on both mechanisms. The other 22 rows rest on reading the binary and the reference, which is exactly what produced the four wrong answers above. Fire a hook at whatever you are about to build on. If it does not fire, look for the precondition before concluding the event is inert - and if it does block on exit 2, that tells you one thing about one exit code, not what kind of hook it is. WorktreeCreate and WorktreeRemove both block, and they are not the same kind of hook at all: an observe-only logger is fatal on one and harmless on the other. That is the whole lesson here, and it cost four revisions of this one number to learn.

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:

json
{
  "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. Returning JSON on stdout is optional, and this is the part people get backwards: you print JSON only when you want to change something. Exit 0 with no output is the normal, correct way to say "carry on". The input always includes session_id and hook_event_name. Tool-related events add tool_name and tool_input.

Input Example (PreToolUse)

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

Output Schema

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command on a protected branch",
    "additionalContext": "Text injected into Claude's context"
  }
}

There is no top-level "decision": "allow". This is the single most common mistake. The top-level decision field takes "approve" or "block" - the v2.1.233 binary throws Unknown hook decision type: allow. Valid types are: approve, block - so "allow" does not silently pass, it errors. In practice you rarely want "approve" either: to let an action through, omit the field, or just exit 0 printing nothing at all.

PreToolUse is the exception with the richer vocabulary, and it lives one level down in hookSpecificOutput:

permissionDecisionEffect
"allow"Tool runs, no prompt
"deny"Tool call is blocked
"ask"User is prompted to confirm
"defer"Falls through to the normal permission flow. Print-mode only - in an interactive session the binary logs defer is print-mode only and ignores it, which looks exactly like your hook never ran

The hooks reference is blunt about the older form: "PreToolUse previously used top-level decision and reason fields, but these are deprecated for this event." The deprecated "approve" and "block" values map to "allow" and "deny". Other events, including PostToolUse and Stop, still use top-level decision as their current format - so the shape genuinely differs by event, and you have to check.

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.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/security-tier-check.py"
          }
        ]
      }
    ]
  }
}
python
#!/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({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": 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({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "ask",
            "permissionDecisionReason": f"WARNING: '{matched}' (tier {max_tier}) detected. Proceed with caution."
        }
    }))
# Nothing to print in the allow case: exit 0 with no output and the tool runs.

Note the shape. permissionDecision and permissionDecisionReason sit inside hookSpecificOutput, and the allow branch prints nothing at all. An older version of this hook used a top-level {"decision": "deny"}, which is the deprecated form for PreToolUse - it is the kind of thing that keeps working until it quietly does not.

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.

python
#!/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 = {
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": (
                f"Delegation score: {score}. "
                f"Consider delegating this task to a subagent."
            )
        }
    }
else:
    output = {}

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-7 to Sonnet, 8+ 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.

bash
#!/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 "{\"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 "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"WARNING: Context estimated at ${NEW_TOTAL} tokens (60%+). Prefer subagent delegation for remaining research. Load summaries, not full docs.\"}}"
fi
# Under both thresholds: print nothing. Silence is how a hook says "carry on".

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.

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

# No output, exit 0: the edit stands and Claude carries on.

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.

python
#!/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({
    "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:

python
#!/usr/bin/env python3
import json, sys

data = json.loads(sys.stdin.read())
# Your logic here

print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": data.get("hook_event_name", ""),
        "additionalContext": "Your message to Claude here"
    }
}))

No decision field: omitting it is how you let the action through. Add {"decision": "block", "reason": "..."} only when you actually want to stop something, and only on an event that supports it.

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 31 hook event types covering the full session lifecycle: setup and session start/end, user prompt submission and expansion, message display, tool execution (pre/post/failure/batch), permission requests and denials, subagent lifecycle, task events, teammate idle, context compaction, configuration changes, directory and file watching, worktree management, notifications, and MCP elicitations. Counted from Anthropic's hooks reference on 2026-08-15.
Which Claude Code hooks can block actions?+
18 of the 31, counted against the v2.1.233 binary rather than the docs summary table, which implies 14. The mechanisms overlap rather than partitioning cleanly. Exit code 2 with the reason on stderr is the general one and blocks for most of the 18. A top-level decision of block also works for many of the same events, including UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, PreCompact and TaskCreated, so the two are not complements. Two events have their own nested shape: PreToolUse via hookSpecificOutput.permissionDecision, and PermissionRequest via hookSpecificOutput.decision.behavior. A few, including TaskCompleted and TeammateIdle, also accept continue false. WorktreeCreate is the exception to all of it: it is a provider rather than a veto, its job is to return the new worktree path, and it aborts worktree creation on exit 1, exit 2, and on exit 0 with no path - so never register an observe-only hook on it. Exit 0 WITH a valid path is the success case. Its sibling WorktreeRemove is an ordinary veto - exit 2 blocks the removal, exit 0 lets it through - but it only fires for a worktree a WorktreeCreate hook provided. The other 13 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 31 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 can both block the prompt and inject additionalContext that Claude reads before responding. Blocking works either by exiting 2 with the reason on stderr, or by returning a top-level decision of block - I tested both against v2.1.233 and each returns num_turns 0 with 'UserPromptSubmit operation blocked by hook'.
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