CC-202: Hook Architecture

Listen instead
CC-202 Hook Architecture
0:00

Understanding Hooks

Hooks are Claude Code's event-driven extension mechanism. They let you intercept, inspect, and modify Claude's behavior at specific points in its lifecycle. If plugins are the packaging system, hooks are the control plane: they enforce policies, inject context, validate outputs, and orchestrate side effects without requiring the user to explicitly invoke anything.

Hooks can be defined in two places: inside a plugin (as plugin hook components) or directly in your settings.json configuration file. Both approaches use the same event model and hook API. Plugin-defined hooks are portable and distributable; settings-defined hooks are quick to configure and ideal for personal or project-level policies.

Official Documentation: The complete hooks reference is at Claude Code Hooks documentation. This module provides architectural depth beyond the reference docs.

Hook Event Types

Claude Code defines nine hook events that cover the full session lifecycle. Understanding when each event fires is fundamental to writing effective hooks.

Event When It Fires Common Use Cases
PreToolUse Before Claude executes any tool call Block dangerous commands, validate file paths, enforce policies
PostToolUse After a tool call completes Log actions, check results, trigger follow-up actions
Stop When Claude finishes its response Validate final output, enforce output format, add disclaimers
SubagentStop When a spawned subagent finishes Review subagent output, aggregate results, quality checks
SessionStart At the beginning of a new session Load context, inject system instructions, initialize state
SessionEnd When a session terminates Save state, generate summaries, cleanup resources
UserPromptSubmit When the user submits a prompt Input validation, prompt augmentation, routing decisions
PreCompact Before context window compaction Save critical context, extract key decisions, persist state
Notification When Claude emits a notification Forward to external systems, logging, alerting

Hook Configuration in settings.json

The fastest way to add a hook is through your settings.json file. Claude Code reads hooks from the hooks key, which maps event names to arrays of hook definitions.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hook": "prompt:Reject any Bash command that contains 'rm -rf /' or 'sudo rm -rf'. Respond with a safety warning.",
        "description": "Blocks catastrophic delete commands"
      }
    ],
    "SessionStart": [
      {
        "hook": "prompt:Remind the user of the current project conventions: we use TypeScript strict mode, Vitest for tests, and follow conventional commits.",
        "description": "Injects project conventions at session start"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hook": "bash:cd ${PROJECT_DIR} && npx eslint --no-error-on-unmatched-pattern ${TOOL_OUTPUT_PATH} 2>/dev/null || true",
        "description": "Auto-lints files after writing"
      }
    ]
  }
}

Each hook definition has three fields:

  • matcher (optional) — A pipe-separated list of tool names to match. When omitted, the hook fires for all tool calls within that event. Only applicable to PreToolUse and PostToolUse events.
  • hook — The hook implementation, prefixed with either prompt: or bash: to indicate the hook type.
  • description — Human-readable description for documentation and debugging.

Prompt-Based Hooks

Prompt-based hooks (prefixed with prompt:) inject instructions into Claude's context. They do not execute code directly. Instead, they add a system-level instruction that Claude follows as part of its reasoning process.

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hook": "prompt:Before running this Bash command, check if it modifies any files in the /production directory. If it does, refuse to execute and explain that production files require manual deployment through the CI/CD pipeline.",
      "description": "Prevents direct production file modifications"
    }
  ]
}

Prompt hooks are powerful because they leverage Claude's reasoning ability. Rather than pattern-matching against a fixed set of rules, Claude can understand intent and context. A prompt hook that says "block commands that could delete important data" will catch rm -rf, truncate, DROP TABLE, and countless other destructive patterns that a simple regex could miss.

When to Use Prompt Hooks

  • Policy enforcement that requires understanding intent, not just syntax
  • Context injection (project conventions, team guidelines, domain knowledge)
  • Output format enforcement (requiring specific structures, disclaimers, or templates)
  • Behavioral modification (changing how Claude approaches a category of tasks)

Bash Hooks

Bash hooks (prefixed with bash:) execute shell commands. They run in a subprocess and can interact with the filesystem, external APIs, and system tools. The exit code determines whether the hook blocks or allows the operation.

{
  "PreToolUse": [
    {
      "matcher": "Write|Edit",
      "hook": "bash:echo '${TOOL_INPUT}' | jq -r '.file_path' | grep -qE '\\.(env|key|pem|secret)' && exit 1 || exit 0",
      "description": "Blocks writes to sensitive file types"
    }
  ],
  "PostToolUse": [
    {
      "matcher": "Bash",
      "hook": "bash:echo \"[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Tool: Bash, Exit: ${TOOL_EXIT_CODE}\" >> ~/.claude/audit.log",
      "description": "Audit logs all Bash executions"
    }
  ]
}

Bash hooks have access to environment variables that provide context about the current tool call:

  • ${TOOL_INPUT} — JSON string of the tool's input parameters
  • ${TOOL_OUTPUT} — The tool's output (only available in PostToolUse)
  • ${TOOL_OUTPUT_PATH} — File path when the tool operated on a specific file
  • ${TOOL_EXIT_CODE} — Exit code from the tool execution (PostToolUse only)
  • ${PROJECT_DIR} — The current project's root directory

When to Use Bash Hooks

  • Automated linting, formatting, or validation after file changes
  • Audit logging to external files or services
  • Integration with CI/CD systems, notification services, or monitoring tools
  • File-based policy checks that need filesystem access

Hook Matchers

Matchers control which tool calls trigger a hook. They are pipe-separated strings of tool names:

// Matches only Bash tool calls
"matcher": "Bash"

// Matches file modification tools
"matcher": "Write|Edit"

// Matches all read operations
"matcher": "Read|Grep|Glob"

// Matches everything (same as omitting matcher)
"matcher": "*"

Matchers are only applicable to PreToolUse and PostToolUse events. For other events like SessionStart or Stop, the matcher field is ignored since these events are not tied to specific tool calls.

Blocking vs Non-Blocking Hooks

Hooks can either block an operation or observe it passively. The behavior depends on the hook type and event:

Blocking Behavior (PreToolUse)

  • Prompt hooks: If the prompt instructs Claude to "block" or "refuse" the operation, Claude will not execute the tool call. The hook acts as an advisory that Claude follows.
  • Bash hooks: If the bash command exits with a non-zero status (exit 1), the tool call is blocked. Exit 0 allows it to proceed.

Non-Blocking Behavior (PostToolUse, SessionStart, etc.)

  • Post-execution hooks observe but do not reverse completed actions.
  • They can log, notify, or inject follow-up instructions, but they cannot undo a tool call that already executed.
  • Bash hooks that fail in PostToolUse log a warning but do not affect the session.

Validation Patterns

Hooks excel at enforcing development policies. Here are proven patterns for common validation needs:

Git Safety Hook

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hook": "prompt:Before executing this Bash command, check for these dangerous git operations: (1) git push --force to main or master, (2) git reset --hard without a specific commit, (3) git clean -fdx in the project root. If any are found, BLOCK the command and suggest a safer alternative.",
      "description": "Prevents destructive git operations"
    }
  ]
}

Secret Detection Hook

{
  "PreToolUse": [
    {
      "matcher": "Write|Edit",
      "hook": "prompt:Before writing this content, scan for potential secrets: API keys (strings starting with sk-, pk-, or matching common API key patterns), passwords in plaintext, private keys, tokens, or connection strings with embedded credentials. If found, BLOCK the write and warn the user.",
      "description": "Prevents accidental secret commits"
    }
  ]
}

Auto-Format on Save

{
  "PostToolUse": [
    {
      "matcher": "Write|Edit",
      "hook": "bash:filepath=$(echo '${TOOL_INPUT}' | jq -r '.file_path // .filePath // empty') && [ -n \"$filepath\" ] && npx prettier --write \"$filepath\" 2>/dev/null || true",
      "description": "Auto-formats files after writing"
    }
  ]
}

Security Enforcement via Hooks

Hooks are a critical security layer for Claude Code deployments, especially in team or enterprise settings. By combining prompt hooks (for intent-aware policy enforcement) with bash hooks (for deterministic validation), you can create a robust security posture.

Defense-in-Depth Example

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hook": "prompt:You are operating in a restricted environment. NEVER execute commands that: install system packages (apt, brew, yum), modify system configuration (/etc), open network listeners (nc -l, python -m http.server), or download executables (curl piped to bash). Block any such command.",
        "description": "System modification guard (prompt layer)"
      },
      {
        "matcher": "Bash",
        "hook": "bash:cmd=$(echo '${TOOL_INPUT}' | jq -r '.command') && echo \"$cmd\" | grep -qiE '(curl|wget).*\\|.*(bash|sh|python|node)' && exit 1 || exit 0",
        "description": "Pipe-to-shell guard (bash layer)"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hook": "bash:echo \"[AUDIT] $(date -u +%Y-%m-%dT%H:%M:%SZ) Bash executed. Exit: ${TOOL_EXIT_CODE}\" >> ~/.claude/security-audit.log",
        "description": "Security audit trail"
      }
    ],
    "PreCompact": [
      {
        "hook": "prompt:Before compacting context, ensure that all security-relevant decisions made in this session are preserved in the summary. Include: any files modified, any commands executed with elevated context, and any policy violations that were caught and blocked.",
        "description": "Preserves security context across compaction"
      }
    ]
  }
}

This layered approach provides two levels of defense: the prompt hook catches intent-level violations (asking Claude to download and run scripts), while the bash hook catches specific syntactic patterns (curl piped to bash) that the prompt hook might miss. The post-execution audit hook creates a paper trail for security review.

Hook Ordering and Execution

When multiple hooks are registered for the same event, they execute in the order they are defined. For PreToolUse hooks, if any hook blocks the operation, subsequent hooks for that event are skipped. This means you should order hooks from most critical (security) to least critical (convenience).

{
  "PreToolUse": [
    { "hook": "...", "description": "Security: block destructive commands" },
    { "hook": "...", "description": "Policy: enforce code style" },
    { "hook": "...", "description": "Convenience: add context hints" }
  ]
}

Key Takeaways

  • Nine hook events cover the full Claude Code lifecycle from session start to end.
  • Two hook types: prompt hooks (intent-aware, advisory) and bash hooks (deterministic, code-based).
  • Matchers filter hooks to specific tool names using pipe-separated lists.
  • PreToolUse hooks can block operations; PostToolUse hooks observe and react.
  • Layer prompt and bash hooks for defense-in-depth security enforcement.
  • Configure hooks in settings.json for quick setup, or in plugins for portability.
  • Order hooks from most critical to least critical within each event.