Applied Intelligence
Module 11: Skills, Hooks, and Automation

The Hooks System

From probabilistic to deterministic

Skills instruct Claude what to do. CLAUDE.md provides guidance the agent should follow. Both rely on Claude choosing to comply.

Hooks take a different approach entirely. A hook is a shell command that executes at a specific lifecycle point, every time, without fail. Claude does not decide whether to run it. The hook fires as deterministic application code, not as a suggestion the agent interprets.

This distinction matters for automation that cannot tolerate "usually works." A PreToolUse hook that blocks writes to .env files does not rely on Claude remembering a policy. It intercepts the operation at the application level and blocks it with certainty. A PostToolUse hook that runs Prettier after every edit does not depend on Claude feeling like formatting. It formats the code every time.

Module 2 introduced hooks as a configuration option. This section covers the full hook system: all events, input/output contracts, and the mechanics that make hooks reliable enough for enterprise automation.

The complete event catalog

Claude Code fires hooks at thirteen lifecycle points. Each event carries specific semantics about when it fires and what control it offers.

EventWhen it firesCan block
SessionStartSession begins or resumesNo
UserPromptSubmitUser submits a promptYes
PreToolUseBefore tool executionYes
PermissionRequestWhen permission dialog appearsYes
PostToolUseAfter tool succeedsNo
PostToolUseFailureAfter tool failsNo
SubagentStartWhen spawning a subagentNo
SubagentStopWhen subagent finishesYes
StopClaude finishes respondingYes
PreCompactBefore context compactionNo
SessionEndSession terminatesNo
NotificationClaude Code sends notificationsNo
SetupRepository initialization via flagsNo

The "Can block" column indicates whether exit code 2 prevents the operation from proceeding. Events that cannot block still receive the hook's output for logging or notification purposes.

SessionStart

Fires when a session begins (startup), resumes from a previous state (resume), gets cleared (clear), or undergoes compaction (compact). The matcher field distinguishes these scenarios:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/load-secrets.sh"
          }
        ]
      }
    ]
  }
}

SessionStart hooks can inject context by writing to stdout. Any output from the hook is added to the conversation context, making hooks useful for loading dynamic information at session start.

UserPromptSubmit

Fires after the user types a prompt but before Claude processes it. This is the interception point for input validation, logging, or transformation.

With exit code 2, the hook blocks prompt processing entirely, erases the prompt from the interface, and shows stderr to the user. This enables guardrails that prevent certain requests from reaching the agent at all.

PreToolUse and PostToolUse

These two events handle most hook automation scenarios.

PreToolUse fires before any tool executes. The hook receives the tool name and complete input parameters via stdin. Exit code 2 blocks the tool call and feeds stderr back to Claude as an error message, allowing the agent to adjust its approach.

PostToolUse fires after successful tool execution. The hook receives the tool input and output. It cannot block because the operation already completed, but it can perform follow-up actions like formatting, testing, or logging.

PostToolUseFailure fires when a tool fails. This enables different handling for errors than successes—alerting on failures while staying quiet on success, for example.

PermissionRequest

Fires when Claude Code would normally show a permission dialog. Hooks can auto-approve operations by returning a JSON response with hookSpecificOutput.permissionDecision: "allow", deny them outright with "deny", or pass through to the user with "ask".

This enables policy-based permission handling that evaluates each request programmatically rather than requiring user interaction for every operation.

Stop and SubagentStop

Stop fires when Claude finishes generating a response. SubagentStop fires when a spawned subagent completes.

Both support blocking via exit code 2, which signals the agent to continue working rather than stopping. This enables hooks that validate completion criteria: did the agent actually finish the task, or did it just stop talking?

PreCompact

Fires before Claude Code compresses conversation context. The matcher distinguishes manual compaction (manual) from automatic compaction triggered by context limits (auto).

Hooks can perform cleanup, save state, or log what context is about to be lost. They cannot prevent compaction from occurring.

Setup

Fires during repository initialization triggered by --init, --init-only, or --maintenance flags. The matcher distinguishes initialization (init) from maintenance runs (maintenance).

This hook enables automatic project setup: installing dependencies, configuring tools, establishing initial state.

Hook input contract

Every hook receives JSON via stdin containing the event context:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../session.jsonl",
  "cwd": "/Users/project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run tests"
  },
  "tool_use_id": "toolu_01ABC123..."
}

The exact fields vary by event. PreToolUse includes tool_name and tool_input. PostToolUse adds tool_output with the execution result. SessionStart includes the session type (startup, resume, etc.).

Hooks parse this JSON to make decisions. A common pattern uses jq to extract relevant fields:

#!/bin/bash
input=$(cat /dev/stdin)
tool_name=$(echo "$input" | jq -r '.tool_name')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')

# Block writes to sensitive files
if [[ "$file_path" == *".env"* ]]; then
  echo "Cannot modify .env files" >&2
  exit 2
fi

exit 0

Exit codes and their meaning

Exit codeMeaningBehavior
0Success/AllowOperation proceeds; stdout parsed for structured output
2BlockOperation prevented; stderr fed back to Claude
OtherErrorWarning logged; operation proceeds

Exit code 2 only blocks operations for events that support blocking. For other events, exit code 2 behaves like any non-zero exit: it logs a warning and continues.

The asymmetry is intentional. PreToolUse blocking prevents potentially harmful operations. PostToolUse cannot block because the operation already happened. Stop blocking keeps Claude working when automated criteria say the task is incomplete.

Structured JSON output

Hooks can return JSON to stdout for fine-grained control:

{
  "continue": true,
  "stopReason": "",
  "suppressOutput": false,
  "systemMessage": "Warning: approaching sensitive data",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Whitelisted command pattern",
    "updatedInput": {
      "command": "npm test --ci"
    },
    "additionalContext": "Note: running in CI mode for deterministic output"
  }
}

Key fields:

  • continue: Whether processing should continue (false stops the session)
  • systemMessage: Injected into conversation as a system message
  • hookSpecificOutput.permissionDecision: For PermissionRequest, auto-resolves the dialog
  • hookSpecificOutput.updatedInput: For PreToolUse, modifies tool parameters before execution
  • hookSpecificOutput.additionalContext: Injects context visible to Claude

The updatedInput field enables input transformation. A PreToolUse hook can intercept rm -rf / and change it to echo "Blocked dangerous command" without fully blocking the operation or causing the agent to interpret the action as a failure.

When a hook returns exit code 2, JSON in stdout is ignored. Only stderr reaches Claude as the error message. Structure your hooks to output JSON only on success (exit 0).

Configuration structure

Hooks live in settings files following the standard hierarchy:

  1. Enterprise managed settings (highest priority)
  2. Project settings (.claude/settings.json)
  3. User settings (~/.claude/settings.json)
  4. Local project settings (.claude/settings.local.json)

The configuration structure:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/validate-bash.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Matchers

The matcher field filters which operations trigger the hook:

  • Exact match: "Write" matches only the Write tool
  • Alternatives: "Edit|Write" matches either
  • Regex: "Notebook.*" matches NotebookEdit, NotebookRead, etc.
  • Wildcard: "*" or "" matches all tools
  • Arguments: "Bash(npm test*)" matches Bash calls starting with npm test

Matchers are case-sensitive. "bash" does not match the Bash tool.

Timeout

Hooks timeout after 60 seconds by default. The timeout field overrides this, accepting values up to 600 seconds (10 minutes).

Long-running hooks should use appropriate timeouts. A hook that runs a test suite needs more time than one that checks a file extension.

The once flag

{
  "matcher": "startup",
  "hooks": [
    {
      "type": "command",
      "command": "load-context.sh",
      "once": true
    }
  ]
}

Setting once: true ensures the hook runs only once per session. Subsequent events matching the same hook are skipped. This prevents redundant initialization or loading.

Environment variables

Hooks receive environment variables for context:

VariableDescription
CLAUDE_PROJECT_DIRAbsolute path to project root
CLAUDE_ENV_FILEPath for persisting environment (SessionStart, Setup only)
CLAUDE_SESSION_IDCurrent session identifier
CLAUDE_CODE_REMOTE"true" if running in web environment

The CLAUDE_ENV_FILE enables SessionStart hooks to persist environment variables across the session. Writing export VAR=value lines to this file makes them available to subsequent hooks.

Security model

Hooks execute with the user's full privileges. They can read any file, make network requests, modify system state. This power requires corresponding caution.

Claude Code captures hook configurations at session startup. If settings files change during a session, the changes do not take effect until the session restarts. This prevents configuration injection attacks where malicious code modifies hook settings mid-session.

When external changes to hook configuration are detected, Claude Code warns the user and requires explicit approval via the /hooks command before applying the new configuration.

Enterprise deployments can set allowManagedHooksOnly: true to block all user, project, and plugin hooks. Only hooks from managed enterprise policies execute, ensuring centralized control over automation behavior.

The /hooks command provides an interactive interface for viewing and managing hook configuration. Use it to verify which hooks are active and troubleshoot unexpected behavior.

Prompt-type hooks

Beyond shell commands, Claude Code supports prompt-based hooks that use an LLM to make decisions:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate if the task is complete: $ARGUMENTS",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

The $ARGUMENTS placeholder gets replaced with the current context. If omitted, the input JSON is appended to the prompt instead.

A separate agent evaluates the prompt and returns:

{
  "ok": true,
  "reason": "All tests pass and documentation is updated"
}

If ok is false and the event supports blocking, the operation is blocked.

Prompt-type hooks bridge deterministic automation with semantic evaluation. A Stop hook can check whether Claude actually finished the task rather than just stopped generating tokens.

Supported events for prompt-type hooks: Stop, SubagentStop, UserPromptSubmit, PreToolUse, PermissionRequest.

Codex comparison

Codex does not have a hooks system. This is the most significant automation gap between the two tools.

Codex offers a limited notification mechanism:

# config.toml
notify = ["python3", "/path/to/notify.py"]

This fires only on agent-turn-complete events. It cannot intercept operations, block tool calls, or modify inputs. The notification receives event data but has no control over Codex behavior.

For automation that requires lifecycle control, Claude Code hooks are currently the only option among the two tools. Codex users who need similar functionality must implement external tooling or CI/CD pipelines to provide the missing guardrails.

The difference reflects architectural priorities. Claude Code treats the CLI as an extensible platform where users customize behavior. Codex treats the CLI as an interface to a cloud-based agent, with less emphasis on client-side programmability.

The next section covers practical hook patterns that leverage this system for real automation workflows.

On this page