architecture

Advanced Hooks: Beyond the Basics

PreToolUse, PostToolUse, Stop, Notification: chain hooks for file protection, token tracking, auto-formatting, and audit logs.

15 min read·

Hooks run outside the LLM loop. No AI reasoning. No token cost. They intercept every tool call, session event, and notification before or after it happens. That gives you deterministic control over what Claude can and cannot do.

This breakdown covers 9 key lifecycle events (out of 30+ in total), the 3-code exit system, context injection, the 3 hook types (shell / HTTP / prompt), chaining patterns, and 5 production-ready examples you can copy immediately.

9+
lifecycle events
0
token cost
3
exit codes

Section 01

Key Lifecycle Events

Every hook fires on a specific lifecycle event. PreToolUse, PostToolUse, and UserPromptSubmit are the workhorses; the rest handle session lifecycle. Most setups only need 3-4 events. (Claude Code exposes 30+ events total; these are the ones you will use most.)

EventFires WhenUse For
PreToolUseBefore any tool executesBlock operations, inject context, validate inputs
PostToolUseAfter any tool completesLint code, format output, log changes
UserPromptSubmitBefore Claude sees the user messageInject just-in-time context, append metadata, reject prompt on exit 2
SessionStartSession opensLoad dynamic context, check environment
StopClaude finishes respondingNotify user, log cost, trigger downstream
NotificationClaude needs user inputSound alert, system notification
InstructionsLoadedCLAUDE.md files loadedDebug which rules loaded, log active config
WorktreeCreateNew worktree createdCustom initialization, env setup
WorktreeRemoveWorktree removedCustom teardown, cleanup temp files

PreToolUse and PostToolUse are the workhorses. The other 6 handle session lifecycle. Most setups only need 3-4 events total.

Section 02

The Exit Code System

Your hook's exit code tells Claude what to do next. Three possible outcomes, each with different behavior.

0
Allow

Proceed as planned. The tool call or event continues normally. If the hook outputs JSON, Claude receives it as context.

1
Non-blocking Error

An error occurred in the hook but Claude continues the action anyway. Claude logs a notice with the first line of stderr. This does NOT block or prompt a retry. Only exit code 2 actually blocks.

2
Block

Hard stop. Claude cannot proceed with this action. Your hook's stderr becomes the error message Claude sees.

#!/bin/bash
# Example: block dangerous bash commands

COMMAND="$CLAUDE_TOOL_INPUT_COMMAND"

if echo "$COMMAND" | grep -qE "rm -rf|git push --force|git reset --hard"; then
  echo "BLOCKED: dangerous command detected" >&2
  exit 2   # Hard block
fi

exit 0     # Allow everything else

Section 03

Context Injection

When a hook returns JSON to stdout, Claude reads it as a message. This creates a feedback loop where your tools inform the AI without spending tokens on reasoning.

1PostToolUse hook fires after Claude edits a file
2Hook runs ESLint on the edited file, finds 3 errors
3Hook outputs JSON to stdout with the error details
4Claude reads the message and auto-fixes the errors without you asking
#!/bin/bash
# PostToolUse hook: auto-lint after edits

FILE="$CLAUDE_TOOL_INPUT_FILE_PATH"

# Only lint JS/TS files
if [[ "$FILE" =~ \.(js|ts|jsx|tsx)$ ]]; then
  RESULT=$(npx eslint "$FILE" 2>&1)
  if [ $? -ne 0 ]; then
    # Return JSON that Claude will read as a message
    echo "{\"type\": \"text\", \"text\": \"ESLint found errors in $FILE:\\n$RESULT\"}"
    exit 0  # exit 0 = allow, but Claude sees the message
  fi
fi

exit 0

Key insight: exit 0 + JSON stdout = Claude proceeds AND receives your context. This is how you build feedback loops without blocking anything.

Section 04

The 3 Hook Types

Most people only know shell scripts. Two more types exist and they unlock patterns that shell scripts cannot reach.

Type A: Shell Script Hooks

The basics. Most people know this one.

A shell script that runs on the event. Receives tool input via environment variables. Returns exit code + optional JSON. Full access to the filesystem, can run any language, best for file ops, linting, git checks, and system commands.

// settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": { "tool": "Bash" },
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/bash-safety.sh"
          }
        ]
      }
    ]
  }
}

Type B: HTTP Hooks

Call external services. Almost nobody uses these.

Instead of running a script, send an HTTP request to an endpoint. The response body becomes the hook output. Perfect for triggering Slack notifications, updating Notion pages, calling webhooks, or logging to dashboards.

// settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": { "tool": "Bash", "command": "git push" },
        "hooks": [
          {
            "type": "http",
            "url": "https://hooks.slack.com/services/T00/B00/xxx",
            "method": "POST",
            "headers": { "Content-Type": "application/json" },
            "body": "{\"text\": \"Code pushed to remote.\"}"
          }
        ]
      }
    ]
  }
}

Type C: Prompt Hooks

AI-powered safety checks. Almost nobody knows these exist.

Run an LLM on the hook event data. The prompt receives the tool input and returns a judgment. Use this for nuanced decisions that regex cannot handle: semantic safety checks, context-aware permissions, natural language policy enforcement.

// settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": { "tool": "Bash" },
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Is this bash command safe to run in a dev environment? If it could delete data, modify system files, or affect production, respond with EXIT_CODE=2 and explain why. Otherwise respond EXIT_CODE=0. Command: {{input.command}}"
          }
        ]
      }
    ]
  }
}
Cost note: Prompt hooks DO consume tokens since they invoke an LLM. Use sparingly on high-risk events only. Shell hooks remain at zero token cost.

Section 05

Chaining Patterns

Multiple hooks can fire on the same event in sequence. The first blocking exit 2 stops the chain; subsequent hooks in the array do not run.

Guard → Log

A synchronous safety check runs first (can block). An async logging hook runs second, only if the action was allowed.

Lint → Notify

PostToolUse lints the edited file and returns JSON context. A second HTTP hook notifies the team channel.

Validate → Backup

PreToolUse validates the file path is allowed. A second hook snapshots the current file before the edit lands.

Scan → Audit

Secret scanner checks staged files before git commit. An audit log hook records the commit attempt regardless of outcome.

// settings.json: chained hooks on one event
{
  "hooks": {
    "PostToolUse": [{
      "matcher": { "tool": "Edit", "path": "*config*" },
      "hooks": [
        {
          "type": "command",
          "command": "bash ~/.claude/hooks/audit-log.sh"
        },
        {
          "type": "http",
          "url": "https://hooks.slack.com/services/T00/B00/xxx",
          "method": "POST",
          "headers": { "Content-Type": "application/json" },
          "body": "{\"text\": \"Config file modified by Claude Code.\"}",
          "async": true
        }
      ]
    }]
  }
}

Section 06

5 Production Examples

Ready-to-copy implementations. Click any pattern to expand the full code.

Section 07

settings.json Reference

The full settings.json structure for a production hook setup: async logging on every tool, file protection on writes, and cost tracking on stop.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": { "tool": "Write" },
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/client-protect.sh"
          }
        ]
      },
      {
        "matcher": { "tool": "Bash", "command": "git commit" },
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/secret-scan.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": { "tool": "Edit" },
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/auto-lint.sh"
          }
        ]
      },
      {
        "matcher": {},
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/change-log.sh",
            "async": true
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": {},
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.claude/hooks/cost-tracker.sh",
            "async": true
          }
        ]
      }
    ]
  }
}

Section 08

Debug Tips

Hooks fail silently by default. These patterns make them observable.

Log all env vars at hook start

Run `env | grep CLAUDE_` at the top of your hook during development to see exactly what variables are available for your event type.

Test exit codes in isolation

Write a minimal hook that only exits with a fixed code and verify Claude's behavior before adding real logic. exit 2 should halt the action every time.

stderr goes to Claude, stdout is data

Anything printed to stderr appears as the error message Claude sees on exit 2. Anything printed to stdout (as JSON) becomes context Claude can act on.

Use async: true for logging hooks

Any hook that only logs, notifies, or syncs state should be async. Synchronous hooks add latency to every tool call; keep them lean.

Chain hooks for layered enforcement

Place multiple hooks in the hooks array under one matcher. They run in order. A blocking exit 2 from the first stops the rest from running.

#!/bin/bash
# Debug header: add to top of any hook during development

echo "=== HOOK DEBUG ===" >&2
echo "Tool: $CLAUDE_TOOL_NAME" >&2
echo "File: $CLAUDE_TOOL_INPUT_FILE_PATH" >&2
echo "Command: $CLAUDE_TOOL_INPUT_COMMAND" >&2
echo "Project: $CLAUDE_PROJECT_DIR" >&2
echo "==================" >&2

# ...rest of your hook logic

For a hook that prevents secrets from reaching git, see our Claude Code security audit guide. For how hooks fit into the full context engineering system, see Context Engineering Mastery. Need production hook setup for your team? Book a consultation.

Want this running in your stack?

AY Automate builds agent systems, RAG pipelines, and Claude Code setups for production teams.