Build This Now
Build This Now
Keyboard ShortcutsStatus Line Guide
Hooks GuideCross-Platform Hooks for Claude CodeClaude Code Setup HooksStop HooksSelf-Validating Claude Code AgentsClaude Code Session HooksContext Backup Hooks for Claude CodeSkill Activation HookClaude Code Permission Hook
Get Build This Now
speedy_devvkoen_salo
Blog/Toolkit/Hooks/Hooks Guide

Hooks Guide

Claude Code hooks from first principles: exit codes, JSON output, async, HTTP, and production patterns.

Problem: You approve every file write by hand. Then every command. Then every format pass. Twenty interruptions later, the feature you were building has slipped out of your head.

Quick Win: Add this to .claude/settings.json and never approve a Prettier format again:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\""
          }
        ]
      }
    ]
  }
}

Every file Claude writes now auto-formats. Zero clicks. Zero context switches.

The 12 Hook Lifecycle Events

Every Claude Code event can trigger a hook. A hook runs a shell command, or sends an LLM prompt, when that event fires. The full list:

HookWhen It FiresCan Block?Best Use
SessionStartSession begins or resumesNOLoad context, set env vars
UserPromptSubmitYou hit enterYESContext injection, validation
PreToolUseBefore tool runsYESSecurity blocking, auto-approve (extends permission system)
PermissionRequestPermission dialog appearsYESAuto-approve/deny
PostToolUseAfter tool succeedsNO*Auto-format, lint, log
PostToolUseFailureAfter tool failsNOError handling
SubagentStartSpawning subagentNOSubagent initialization
SubagentStopSubagent finishesYESSubagent validation
StopClaude finishes respondingYESTask enforcement
PreCompactBefore compactionNOTranscript backup
SetupWith --init/--maintenanceNOOne-time setup
SessionEndSession terminatesNOCleanup, logging
NotificationClaude sends notificationNODesktop alerts, TTS

*A PostToolUse hook can feed a message back to Claude, but it cannot undo the tool that already ran.

Exit Codes: The Control Mechanism

Exit codes are how a hook talks to Claude:

Exit CodeWhat Happens
0Success - hook ran, stdout processed for JSON
2Block - operation stopped, stderr sent to Claude
OtherError - stderr shown to user, execution continues

Exit code 2 is the one that blocks things. Return it from a PreToolUse hook and the tool never runs. Return it from a Stop hook and Claude has to keep going instead of finishing.

Hook Types: Command, HTTP, Prompt, and Agent

Four handler types exist. Pick whichever one matches the job.

Command hooks run shell scripts:

{
  "type": "command",
  "command": "python validator.py",
  "timeout": 30
}

HTTP hooks POST to an endpoint and receive JSON back: New - Feb 2026

{
  "type": "http",
  "url": "http://localhost:8080/hooks/pre-tool-use",
  "timeout": 30,
  "headers": {
    "Authorization": "Bearer $MY_TOKEN"
  },
  "allowedEnvVars": ["MY_TOKEN"]
}

Claude Code posts the event JSON as the request body (Content-Type: application/json), and the reply follows whichever JSON output schema a command hook would have used. A few things differ:

  • Non-blocking errors: A non-2xx response, a connection failure, or a timeout will not stop execution. To actually block a tool call, return a 2xx response with decision: "block" in the JSON body.
  • Header env vars: Reference variables with $VAR_NAME or ${VAR_NAME} inside header values. Only names listed in allowedEnvVars get resolved. Anything else resolves to an empty string.
  • Deduplication: HTTP hooks dedupe by URL. Command hooks dedupe by command string.
  • Config only: You have to edit the settings JSON directly. The /hooks interactive menu only supports command hooks.

Prompt hooks use LLM evaluation (a nice fit for Stop and SubagentStop):

{
  "type": "prompt",
  "prompt": "Evaluate if Claude should stop: $ARGUMENTS. Check if all tasks complete.",
  "timeout": 30
}

The LLM replies with {"ok": true} or {"ok": false, "reason": "..."}.

Agent hooks start a subagent that can read files (Read, Grep, Glob) for a deeper check:

{
  "type": "agent",
  "prompt": "Verify all test files have corresponding implementation files",
  "timeout": 60
}

An agent hook pokes around the codebase first, then decides. That makes it more thorough than a prompt hook. It is also slower (default timeout: 60s versus 30s for prompts).

Async Hooks (Non-Blocking) New - Jan 2026

Set async: true and the hook runs in the background instead of blocking Claude. Anthropic shipped this in January 2026:

{
  "type": "command",
  "command": "node backup-script.js",
  "async": true,
  "timeout": 30
}

Best for:

  • Logging and analytics
  • Backup creation (PreCompact)
  • Notifications
  • Any side-effect that shouldn't slow things down

Not suitable for:

  • Security blocking (PreToolUse with exit code 2)
  • Auto-approve decisions (PermissionRequest)
  • Any hook where Claude needs the result

HTTP Hooks: Post to Endpoints New - Feb 2026

Point an HTTP hook at a web URL you own and the event flows there instead of into a local script. That opens up a handful of patterns that command hooks couldn't handle cleanly:

  • Remote validation services that enforce team-wide policies
  • Centralized logging to a shared audit system
  • Webhook integrations with Slack, PagerDuty, or custom dashboards
  • Microservice architectures where hook logic runs alongside your API

Basic HTTP Hook

Route all Bash commands through a validation endpoint:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "http",
            "url": "http://localhost:8080/hooks/pre-tool-use",
            "timeout": 30,
            "headers": {
              "Authorization": "Bearer $MY_TOKEN"
            },
            "allowedEnvVars": ["MY_TOKEN"]
          }
        ]
      }
    ]
  }
}

What lands on your server is the exact stdin payload a command hook would read, delivered through the POST body. Reply in the JSON output format and Claude Code parses it the same way.

HTTP Response Handling

HTTP hooks read responses differently. No exit codes. Just HTTP status and the response body:

ResponseBehavior
2xx + empty bodySuccess, equivalent to exit code 0 with no output
2xx + plain text bodySuccess, the text is added as context to Claude
2xx + JSON bodySuccess, parsed using the same JSON output schema as command hooks
Non-2xx statusNon-blocking error, execution continues
Connection failure / timeoutNon-blocking error, execution continues

The thing to watch: An HTTP hook cannot block on status codes alone. Send back a 4xx or a 5xx and you get a logged error while execution rolls on. Real blocking, or a real permission denial, needs a 2xx response whose JSON body carries the decision fields:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Blocked by security policy"
  }
}

Secure Header Authentication

The headers field can interpolate environment variables, but only when the variable appears in allowedEnvVars. That stops secrets from leaking by accident:

{
  "type": "http",
  "url": "https://hooks.example.com/validate",
  "headers": {
    "Authorization": "Bearer $API_KEY",
    "X-Team-Id": "$TEAM_ID"
  },
  "allowedEnvVars": ["API_KEY", "TEAM_ID"]
}

A $VAR that isn't on the allowlist silently becomes an empty string. No warning. No error. Just empty. Review the allowlist twice before you ship.

JSON Output: Advanced Control

When an exit code is too blunt, a hook can hand back a structured JSON response instead.

PreToolUse Decisions

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Safe read operation",
    "updatedInput": { "command": "modified-command" },
    "additionalContext": "Context for Claude"
  }
}
  • "allow": Bypasses permission system
  • "deny": Blocks tool, tells Claude why
  • "ask": Prompts user for confirmation
  • updatedInput: Modify tool parameters before execution

PermissionRequest Decisions

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow",
      "updatedInput": { "command": "npm run lint" }
    }
  }
}

Stop/SubagentStop Enforcement

{
  "decision": "block",
  "reason": "Tests failing. Fix them before completing."
}

Hook 1: Auto-Format on Save

Run formatters after every file write:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\""
          },
          {
            "type": "command",
            "command": "npx eslint --fix \"$CLAUDE_TOOL_INPUT_FILE_PATH\""
          }
        ]
      }
    ]
  }
}

Both hooks run in parallel. Format and lint both finish before Claude's reply appears.

Hook 2: Session Context Injection

Load context at session start:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo '## Git Status' && git status --short && echo '## TODOs' && grep -r 'TODO:' src/ | head -5"
          }
        ]
      }
    ]
  }
}

Persist Environment Variables

A SessionStart or Setup hook can define environment variables that stick for the whole session:

#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
  echo 'export API_KEY=your-key' >> "$CLAUDE_ENV_FILE"
fi
exit 0

Hook 3: Security Blocking

Stop dangerous operations with PreToolUse:

#!/usr/bin/env python3
import json
import sys
import re
 
DANGEROUS_PATTERNS = [
    r'\brm\s+.*-[a-z]*r[a-z]*f',
    r'sudo\s+rm',
    r'chmod\s+777',
    r'git\s+push\s+--force.*main',
]
 
input_data = json.load(sys.stdin)
if input_data.get('tool_name') == 'Bash':
    command = input_data.get('tool_input', {}).get('command', '')
    for pattern in DANGEROUS_PATTERNS:
        if re.search(pattern, command, re.IGNORECASE):
            print("BLOCKED: Dangerous pattern", file=sys.stderr)
            sys.exit(2)
 
sys.exit(0)

Hook 4: Auto-Approve Safe Commands

Use PermissionRequest to skip the prompt when a command is known safe:

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python .claude/hooks/auto-approve.py"
          }
        ]
      }
    ]
  }
}
#!/usr/bin/env python3
import json
import sys
 
SAFE_PREFIXES = ['npm test', 'npm run lint', 'git status', 'ls']
 
input_data = json.load(sys.stdin)
command = input_data.get('tool_input', {}).get('command', '')
 
for prefix in SAFE_PREFIXES:
    if command.startswith(prefix):
        output = {
            "hookSpecificOutput": {
                "hookEventName": "PermissionRequest",
                "decision": {"behavior": "allow"}
            }
        }
        print(json.dumps(output))
        sys.exit(0)
 
sys.exit(0)  # Normal flow for other commands

Hook 5: Transcript Backup

PreCompact is the moment to snapshot a transcript. Mark the hook async: true since a backup never has to block Claude:

{
  "hooks": {
    "PreCompact": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python .claude/hooks/backup-transcript.py",
            "async": true
          }
        ]
      }
    ]
  }
}

Matchers manual and auto separate /compact from automatic compaction.

Hook 6: Task Completion Enforcement

A Stop hook keeps Claude from calling a job done too early:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if all tasks are complete: $ARGUMENTS. Return {\"ok\": false, \"reason\": \"...\"} if work remains."
          }
        ]
      }
    ]
  }
}

The Stop Hook guide covers command-based versions of this pattern.

Hook 7: Skill Activation

A Skill Activation Hook steps in on the prompt before Claude sees it and tacks skill recommendations onto the end. Each keyword you type is checked against a rule set, so the Postgres tuning skill lands in context the moment you mention a slow query. Twenty-one skill categories already use this pattern inside the Code Kit's SkillActivationHook:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node .claude/hooks/SkillActivationHook/skill-activation-prompt.mjs"
          }
        ]
      }
    ]
  }
}

Hook 8: Threshold-Based Backups via StatusLine

StatusLine is the only place you get live context metrics. That makes it the right spot to kick off a backup when you hit a threshold:

{
  "statusLine": {
    "type": "command",
    "command": "node .claude/hooks/ContextRecoveryHook/statusline-monitor.mjs"
  }
}

Critical: The remaining_percentage field already bakes in a fixed 33K-token autocompact buffer. To get the real "free until autocompact" number, do the math yourself:

const AUTOCOMPACT_BUFFER_TOKENS = 33000;
const autocompactBufferPct = (AUTOCOMPACT_BUFFER_TOKENS / windowSize) * 100;
const freeUntilCompact = Math.max(0, pctRemainTotal - autocompactBufferPct);

Two trigger systems run side by side inside the backup script. Tokens are the primary trigger. Percentages sit underneath as a safety net:

// Token-based triggers (primary - works across all window sizes)
const TOKEN_FIRST_BACKUP = 50000; // First backup at 50k tokens used
const TOKEN_UPDATE_INTERVAL = 10000; // Update every 10k tokens after
 
if (currentTotalTokens >= TOKEN_FIRST_BACKUP) {
  if (lastBackupTokens < TOKEN_FIRST_BACKUP) {
    runBackup(
      sessionId,
      `tokens_${Math.round(currentTotalTokens / 1000)}k_first`,
    );
  } else if (currentTotalTokens - lastBackupTokens >= TOKEN_UPDATE_INTERVAL) {
    runBackup(
      sessionId,
      `tokens_${Math.round(currentTotalTokens / 1000)}k_update`,
    );
  }
}
 
// Percentage-based triggers (safety net, especially for 200k windows)
const THRESHOLDS = [30, 15, 5];
for (const threshold of THRESHOLDS) {
  if (state.lastFree > threshold && freeUntilCompact <= threshold) {
    runBackup(sessionId, `crossed_${threshold}pct`);
  }
}
if (freeUntilCompact < 5 && freeUntilCompact < state.lastFree) {
  runBackup(sessionId, "continuous");
}

PreCompact only fires at the moment of compaction. StatusLine-based backups snapshot the session ahead of time, while nothing has broken yet. The token trigger matters most on large windows like 1M, where a percentage trigger would arrive way too late to help.

Architecture: Three-File Structure

The backup system splits into three files with separate jobs:

.claude/hooks/ContextRecoveryHook/
├── backup-core.mjs        # Shared backup logic (parsing, formatting, saving)
├── statusline-monitor.mjs # Threshold detection + display (calls backup-core)
└── conv-backup.mjs        # PreCompact trigger (calls backup-core)
FileTriggerResponsibility
backup-core.mjsCalled by othersParse transcript, format markdown, save file, update state
statusline-monitor.mjsStatusLine (continuous)Monitor tokens/context %, detect triggers, display status
conv-backup.mjsPreCompact hookHandle pre-compaction event

One file owns the actual backup work. Tweak how markdown renders, how files get named, or how state is recorded inside backup-core.mjs, and both callers inherit the change for free.

Backup File Naming

Backups use numbered filenames with timestamps so the history is easy to scan:

.claude/backups/1-backup-26th-Jan-2026-4-30pm.md
.claude/backups/2-backup-26th-Jan-2026-5-15pm.md
.claude/backups/3-backup-26th-Jan-2026-5-45pm.md

StatusLine Display

If a backup exists for the current session, the statusline prints its path right there:

[!] 25.0% free (50.0K/200K)
-> .claude/backups/3-backup-26th-Jan-2026-5-45pm.md

You know at a glance which file to pull back in once compaction runs.

State Tracking

Both hooks share a state file at ~/.claude/claudefast-statusline-state.json:

{
  "sessionId": "abc123",
  "lastFreeUntilCompact": 25.5,
  "currentBackupPath": ".claude/backups/3-backup-26th-Jan-2026-5-45pm.md"
}

Recommended workflow: The moment compaction triggers, hit /clear for a clean slate and reload whichever backup path the statusline printed. Otherwise the auto-generated summary and your restored notes end up stepping on each other.

Hook 9: Setup Hooks for Installation and Maintenance

A Setup hook runs before your session starts, triggered by special CLI flags:

claude --init           # Triggers Setup hook with matcher "init"
claude --init-only      # Same as above, but exits after hook (CI-friendly)
claude --maintenance    # Triggers Setup hook with matcher "maintenance"

Wire them up with matchers in settings.json:

{
  "hooks": {
    "Setup": [
      {
        "matcher": "init",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/setup_init.py",
            "timeout": 120
          }
        ]
      },
      {
        "matcher": "maintenance",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/setup_maintenance.py",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Combine flags with prompts: Tack a prompt onto the flag itself.

claude --init "/install"

The hook runs first, deterministic. Then the /install command fires, agentic. One invocation, two execution modes: a scripted setup followed by an agent that reasons over it.

The Setup Hooks Guide walks through the whole pattern end to end, including justfile setups and interactive onboarding flows.

Configuration Locations

LocationScopePriority
Managed policyEnterpriseHighest
.claude/settings.jsonProject (shared)High
.claude/settings.local.jsonProject (personal)Medium
~/.claude/settings.jsonAll projectsLowest

Disabling and Restricting Hooks

Disable All Hooks

If a hook is causing trouble or you just want a clean baseline, flip disableAllHooks to true in your settings:

{
  "disableAllHooks": true
}

Every hook at every scope goes quiet. User, project, local. Reach for this when debugging a runaway hook or when you need a known-clean baseline.

Managed Hook Restrictions

Organizations that need centralized control can set allowManagedHooksOnly to true in managed settings:

{
  "allowManagedHooksOnly": true
}

Flip it on and the only hooks Claude Code will run are the ones declared in managed settings plus SDK hooks. User scope, project scope, plugin scope: all three get ignored. No local hook can tunnel around the org's security posture anymore.

Pair this setting with allowManagedPermissionRulesOnly, which clamps down on permission rules the same way. Together they put admins in charge of the whole surface: permissions themselves, plus every hook that extends them.

Matcher Syntax

PatternMatches
"" or omittedAll tools
"Bash"Only Bash (exact, case-sensitive)
`"WriteEdit"`
"mcp__memory__.*"All memory MCP tools

Critical: Leave no spaces around the |. And matcher strings are case-sensitive.

Event-Specific Matchers

SessionStart: startup, resume, clear, compactPreCompact: manual, autoSetup: init, maintenanceNotification: permission_prompt, idle_prompt, auth_success

Environment Variables

VariableDescription
CLAUDE_PROJECT_DIRProject root (all hooks)
CLAUDE_ENV_FILEPersist env vars (SessionStart, Setup)
CLAUDE_CODE_REMOTE"true" if web, empty if CLI

Debugging

Hook not triggering?

  • Check matcher syntax (case-sensitive, no spaces)
  • Verify settings file location
  • Test: echo '{"session_id":"test"}' | python your-hook.py

Command failing?

  • Add logging: command 2>&1 | tee ~/.claude/hook-debug.log
  • Run with debug: claude --debug
  • Hooks breaking on other operating systems? See cross-platform hook patterns

Infinite loops with Stop?

  • Always check the stop_hook_active flag first

Start With One Hook

Pick the friction point that hurts most:

  • Formatting every file by hand? PostToolUse formatter
  • Approving safe commands all day? PermissionRequest auto-approve
  • Session opens with no context? SessionStart injection
  • Losing progress to compaction? PreCompact backup
  • Tasks getting called done too early? Stop enforcement

One hook. One friction point gone. Then iterate. If you want to skip the setup, the Code Kit arrives with 5 hooks already configured: skill activation, auto-formatting, context recovery, permission automation, and code validation. Every one of them is built on the patterns above.

More in this guide

  • Keyboard Shortcuts
    Configure custom keyboard shortcuts in Claude Code.
  • Status Line Guide
    Set up a custom Claude Code status line showing model name, git branch, cost, and context usage.
  • AI SEO and GEO Optimization
    A rundown of Generative Engine Optimization: how to get content cited inside ChatGPT, Claude, and Perplexity responses instead of just ranked on Google.
  • Claude Code vs Cursor in 2026
    A side-by-side look at Claude Code and Cursor in 2026: agent models, context windows, pricing tiers, and how each tool fits different developer workflows.
  • Claude Code VSCode Extension
    Anthropic's VS Code extension puts Claude Code inside the editor sidebar as a Spark-icon panel, with inline diffs, plan mode, subagents, and MCP support.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

Get Build This Now

Status Line Guide

Set up a custom Claude Code status line showing model name, git branch, cost, and context usage.

Cross-Platform Hooks for Claude Code

Skip the .cmd, .sh, and .ps1 wrappers. Invoke node directly and the same hook file runs on every operating system your team uses.

On this page

The 12 Hook Lifecycle Events
Exit Codes: The Control Mechanism
Hook Types: Command, HTTP, Prompt, and Agent
Async Hooks (Non-Blocking) New - Jan 2026
HTTP Hooks: Post to Endpoints New - Feb 2026
Basic HTTP Hook
HTTP Response Handling
Secure Header Authentication
JSON Output: Advanced Control
PreToolUse Decisions
PermissionRequest Decisions
Stop/SubagentStop Enforcement
Hook 1: Auto-Format on Save
Hook 2: Session Context Injection
Persist Environment Variables
Hook 3: Security Blocking
Hook 4: Auto-Approve Safe Commands
Hook 5: Transcript Backup
Hook 6: Task Completion Enforcement
Hook 7: Skill Activation
Hook 8: Threshold-Based Backups via StatusLine
Architecture: Three-File Structure
Backup File Naming
StatusLine Display
State Tracking
Hook 9: Setup Hooks for Installation and Maintenance
Configuration Locations
Disabling and Restricting Hooks
Disable All Hooks
Managed Hook Restrictions
Matcher Syntax
Event-Specific Matchers
Environment Variables
Debugging
Start With One Hook

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

Get Build This Now