Lifecycle Hooks

Hooks are deterministic scripts that execute at specific points in an agent’s lifecycle. Unlike guidelines or system prompts — which the model may follow — hooks always run. They provide guaranteed automation: every tool call, every session start, every context compaction event can trigger arbitrary code that the agent cannot skip or override.


Universal Hook Architecture

graph TD
    A["User Input"] --> B["PreProcess Hook"]
    B --> C["Agent Reasoning"]
    C --> D["Tool Selection"]
    D --> E["PreToolUse Hook"]
    E --> F{"Gate"}
    F -->|"exit 0 — allow"| G["Tool Execution"]
    F -->|"exit 2 — block"| C
    G --> H["PostToolUse Hook"]
    H --> I["Agent Response"]
    I --> J{"Continue?"}
    J -->|"Yes"| C
    J -->|"No"| K["Output to User"]

Cross-Platform Event Mapping

EventClaude CodeOpenAI CodexGoogle ADKLangGraph
Before ToolPreToolUseGuardrails (input)Before-tool callbacksMiddleware nodes
After ToolPostToolUseGuardrails (output)After-tool callbacksPost-processing nodes
Session StartSessionStartSession initSession creationGraph entry node
PermissionPermissionRequestApproval PolicyAction ConfirmationsHITL nodes
CompletionStopRun completionAgent terminationTerminal nodes
Context OverflowPreCompact/PostCompactCompaction APIN/AState summarization

Hook Types

Hooks come in four flavors, ordered by complexity and latency. On failure, hooks default to fail-open (allow the call); set "on_failure": "fail-closed" for security-critical hooks.

1. Command Hooks (Shell Scripts)

A shell command runs, inspects the input via stdin, and returns an exit code.

{
  "hooks": {
    "PreToolUse": [
      {
        "type": "command",
        "command": "/usr/local/bin/check-tool-safety.sh",
        "timeout_ms": 5000
      }
    ]
  }
}

2. HTTP Hooks (Webhooks)

The hook sends the event payload to an HTTP endpoint and acts on a JSON response containing a decision field (allow or block).

{
  "hooks": {
    "PreToolUse": [
      {
        "type": "http",
        "url": "https://policy.internal.corp/v1/evaluate",
        "method": "POST",
        "headers": {
          "Authorization": "Bearer ${POLICY_TOKEN}"
        },
        "timeout_ms": 10000
      }
    ]
  }
}

3. LLM Hooks (Prompt-Based)

A second model evaluates the action — useful when the decision requires judgment that cannot be reduced to a regex or a lookup table.

{
  "hooks": {
    "PreToolUse": [
      {
        "type": "llm",
        "model": "claude-sonnet-4-20250514",
        "prompt": "You are a security reviewer. Evaluate the following tool call and decide if it is safe. Respond with JSON: {\"decision\": \"allow\"} or {\"decision\": \"block\", \"reason\": \"...\"}.",
        "timeout_ms": 30000
      }
    ]
  }
}

4. Agent Hooks (Sub-Agent Validation)

A full sub-agent — with its own tool access — validates the action, enabling multi-step reasoning such as inspecting both a schema and its data before approving a migration.

{
  "hooks": {
    "PreToolUse": [
      {
        "type": "agent",
        "agent": {
          "model": "claude-sonnet-4-20250514",
          "system": "You are a database migration safety agent. You have access to tools. Verify that the proposed migration will not cause data loss.",
          "tools": ["Read", "Bash"],
          "max_turns": 5
        },
        "timeout_ms": 120000
      }
    ]
  }
}

Hook Input/Output Protocol

Input

The runtime sends a JSON payload via stdin (for command hooks) or request body (for HTTP hooks):

{
  "event": "PreToolUse",
  "session_id": "sess_abc123",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/build-artifacts"
  },
  "timestamp": "2026-03-28T14:32:01Z",
  "context": {
    "working_directory": "/Users/x/project",
    "session_source": "cli",
    "turn_number": 7,
    "model": "claude-opus-4-6"
  }
}

Output

Exit CodeMeaning
0Proceed. The tool call is allowed.
2Block. The tool call is rejected. The agent sees the reason and can retry with a different approach.

Any other exit code is treated as a hook failure. Optional JSON on stdout allows structured feedback:

{
  "decision": "block",
  "reason": "Command matches destructive pattern: rm -rf on non-tmp path.",
  "suggestions": [
    "Use a more targeted rm command",
    "Move files to trash instead of deleting"
  ]
}

When the hook blocks, the reason field is injected into the agent’s context so it understands why the action was rejected and can adapt.


Matchers

Matchers are regex-based filters that control when a hook activates. All matcher fields must match (logical AND). For OR logic, define multiple hook entries.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": {
          "tool_name": "^(Bash|Write)$"
        },
        "type": "command",
        "command": "/usr/local/bin/check-destructive.sh"
      }
    ]
  }
}

Practical Examples

Block Destructive Commands

Prevent force-pushes to main, destructive production database operations, and similar dangerous commands before they execute.

#!/usr/bin/env bash
# hooks/block-destructive.sh
set -euo pipefail

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

if [[ "$TOOL_NAME" != "Bash" ]]; then
  exit 0
fi

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

# Block force-pushes to main
if echo "$COMMAND" | grep -qP 'git\s+push\s+.*--force.*\b(main|master)\b'; then
  jq -n '{"decision":"block","reason":"Force-push to main/master is not allowed."}'
  exit 2
fi

# Block production database access
if echo "$COMMAND" | grep -qPi '(DROP|TRUNCATE|DELETE\s+FROM)\s.*(prod|production)'; then
  jq -n '{"decision":"block","reason":"Destructive operations on production databases are prohibited."}'
  exit 2
fi

exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": { "tool_name": "^Bash$" },
        "type": "command",
        "command": "bash hooks/block-destructive.sh"
      }
    ]
  }
}

Auto-Format After File Writes

Run a formatter every time the agent writes a file, so the codebase stays consistent without relying on the model to format correctly.

#!/usr/bin/env bash
# hooks/auto-format.sh
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -z "$FILE_PATH" ]]; then
  exit 0
fi

# Only format known file types
case "$FILE_PATH" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md)
    npx prettier --write "$FILE_PATH" 2>/dev/null || true
    ;;
  *.py)
    python -m black "$FILE_PATH" 2>/dev/null || true
    ;;
  *.go)
    gofmt -w "$FILE_PATH" 2>/dev/null || true
    ;;
esac

exit 0

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": {
          "tool_name": "^Write$"
        },
        "type": "command",
        "command": "bash hooks/auto-format.sh"
      }
    ]
  }
}

Secret Detection on Commit

Scan staged files for secrets before the agent can commit them.

#!/usr/bin/env bash
# hooks/secret-scan.sh
set -euo pipefail

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Only trigger on git commit commands
if ! echo "$COMMAND" | grep -qP 'git\s+commit'; then
  exit 0
fi

# Run secret detection on staged files
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null)

if [[ -z "$STAGED_FILES" ]]; then
  exit 0
fi

SECRETS_FOUND=0
REPORT=""

while IFS= read -r file; do
  if [[ ! -f "$file" ]]; then
    continue
  fi

  # Check for common secret patterns
  MATCHES=$(grep -nPi \
    '(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{48}|ghp_[a-zA-Z0-9]{36}|-----BEGIN (RSA |EC )?PRIVATE KEY-----|password\s*=\s*["\x27][^"\x27]{8,})' \
    "$file" 2>/dev/null || true)

  if [[ -n "$MATCHES" ]]; then
    SECRETS_FOUND=1
    REPORT+="$file:\n$MATCHES\n\n"
  fi
done <<< "$STAGED_FILES"

if [[ "$SECRETS_FOUND" -eq 1 ]]; then
  jq -n \
    --arg reason "Potential secrets detected in staged files. Remove them before committing." \
    --arg details "$REPORT" \
    '{"decision":"block","reason":$reason,"details":$details}'
  exit 2
fi

exit 0