Home/Patterns/Advanced Patterns/Hooks and Extension Points

Hooks and Extension Points

How typed interceptors modify agent behavior at 27+ lifecycle events without touching core loop code: four execution modes, condition syntax, and error isolation.

Hooks are the boundary between the agent core and everything else. If you want to validate a tool call before it runs, classify user input before it reaches the agent, audit a completed action, or trigger a webhook on every session start, you do all of that through hooks. The agent loop doesn't know any of this is happening. It calls hooks at defined extension points and continues.

Most systems start with a simple before/after model: run a function before the tool, run another function after. That covers 20% of real production needs. The other 80% requires extension points at session boundaries, memory compaction events, permission decisions, file system changes, and multi-agent team coordination. A before/after model forces you to either pack all of that into the tool hooks (wrong layering) or patch the core loop (unsafe coupling). The solution is a richer event taxonomy: 27+ named lifecycle events organized by phase, each independently hookable.

The mental model that makes this system work: hooks are configured as data, not code. You declare what kind of hook to run, which events to react to, and which conditions must be true. The agent loop interprets all of this. The hook runner evaluates conditions, spawns the right executor, and aggregates results. This separation keeps hooks safe: conditions are pattern matching, not arbitrary code evaluation. You can't accidentally inject logic into the evaluation path itself.

The Four Execution Modes

Every hook declaration specifies one of four execution modes. The modes differ in cost, capability, and latency. Choosing the right mode is the primary design decision for any hook:

ModeExecutionBest ForRelative Cost
commandShell subprocessScripts, formatters, linters, validatorsLow
promptSingle LLM call (small/fast model)Classification, content moderation, intent checksMedium
agentFull multi-turn sub-agent with toolsComplex verification that requires explorationHigh
httpHTTP POST with SSRF protectionExternal webhooks, audit logs, third-party integrationsLow to Medium

The mode is the cost/capability trade-off made explicit. A command hook runs fast and deterministically. It's the right choice for validation logic you can express in a script. A prompt hook makes one LLM call and returns a structured decision, making it right for classification that requires language understanding. An agent hook spawns a full sub-agent with access to tools. It can read files, run commands, and reason across multiple steps, but it's expensive. An http hook posts to an external endpoint and optionally waits for a response, making it right for audit systems and third-party integrations.

The hook configuration structure binds a mode to an event:

# Hooks are configured as data: the runner interprets them
hooks_config:
  PreToolUse:
    - matcher: "Write"
      hooks:
        - type: "command"
          command: "lint-check --file $tool_input_path"
          if: "Write(src/**/*.ts)"
          timeout: 10

  Stop:
    - hooks:
        - type: "agent"
          prompt: "Verify that the implementation matches the requirements. Read the test output and check all tests pass."
          timeout: 60

  PostToolUse:
    - matcher: "Bash"
      hooks:
        - type: "http"
          url: "http://localhost:9000/audit"
          async: true    # fire-and-forget: don't block the agent

Note: The async: true flag signals fire-and-forget. The hook delivers its result asynchronously. The agent doesn't wait. Use this for audit and logging hooks where you want observability without adding latency.

The Hook Event Lifecycle

The 27+ hook events are organized by lifecycle phase. Understanding which phase covers your use case tells you which events to hook:

Session lifecycle: fires once per session, not per turn:

  • SessionStart: agent session begins
  • SessionEnd: agent session ends (clean or error)
  • Setup: initialization phase before the first user interaction

Per-turn: fires around each agent turn:

  • UserPromptSubmit: user submits a prompt, before it reaches the agent
  • Stop: agent decides to stop (turn ends normally)
  • StopFailure: agent hits a termination condition due to an error

Tool lifecycle: fires around every tool invocation:

  • PreToolUse: before a tool executes. Can modify tool arguments or block execution.
  • PostToolUse: after a successful tool execution. Can modify the tool output.
  • PostToolUseFailure: after a tool execution that produced an error
  • PermissionRequest: agent requests permission for a restricted operation
  • PermissionDenied: permission was denied

Memory: fires around context compaction:

  • PreCompact: before the context window is compacted
  • PostCompact: after compaction completes

Multi-agent team: fires in multi-agent coordination scenarios:

  • SubagentStart: a sub-agent is spawned
  • SubagentStop: a sub-agent completes or is cancelled
  • TeammateIdle: a coordinated agent has no pending work
  • TaskCreated: a new task is created in the task queue
  • TaskCompleted: a task completes

Notifications:

  • Notification: the agent emits a notification event

MCP integration:

  • Elicitation: an MCP server requests information from the user
  • ElicitationResult: the result of an elicitation is available

Configuration:

  • ConfigChange: agent configuration is reloaded or modified
  • InstructionsLoaded: system instructions are loaded

File system:

  • CwdChanged: working directory changes
  • FileChanged: a tracked file is modified
  • WorktreeCreate: a new git worktree is created
  • WorktreeRemove: a git worktree is removed

The scope of this event set is the production insight: this is not a simple before/after tool hook system. It covers the entire agent operational surface, including session lifecycle, user turns, tool dispatch, memory management, multi-agent coordination, external integrations, and file system changes. Any of these events can be extended without modifying the core loop.

Condition Syntax

Each hook can declare an if condition that gates when it fires. The condition syntax reuses the permission-rule pattern matching syntax, the same evaluator and the same mental model as the permission system.

# Matches Write tool on any .ts file in src/
if: "Write(src/**/*.ts)"

# Matches any Bash command starting with 'git push'
if: "Bash(git push*)"

# No condition field = always run for this event
# (a hook with no if field matches every invocation)

The condition is evaluated against the tool name and the tool input before the hook executor is even spawned. A non-matching hook is never invoked, meaning no process, no LLM call, no HTTP request. This makes conditions a cost-saving mechanism, not just a filtering mechanism.

The practical implication: write the most specific condition you can. A PreToolUse hook with if: "Write(src/**/*.ts)" fires only for TypeScript source files. The same hook without an if condition fires for every Write call to every file, including build artifacts, logs, and temporary files, most of which you don't care about.

# Example: condition syntax matching structure
hook_matcher:
  matcher: "Write"            # match on tool_name for PreToolUse/PostToolUse
  hooks:
    - type: "command"
      command: "check-types --file $tool_input_path"
      if: "Write(src/**/*.ts)"   # pattern: tool_name(tool_input_glob)
      timeout: 10

The matcher and the if condition work at two levels. The matcher field narrows which tool type triggers this hook group at all. The if condition within each hook further narrows by the specific tool input. Use matcher for broad categorization (all Write calls) and if for fine-grained filtering (Write calls to specific paths or patterns).

Hook Response Protocol

A hook communicates its decision back to the agent loop via a structured response. The response is a discriminated union on whether execution was synchronous or asynchronous:

Synchronous response (hook result is available immediately):

FieldPurpose
continueWhether agent execution should continue
suppressOutputWhether the hook's output should be hidden from the user
stopReasonIf stopping, why. This is shown to the user.
decisionAn explicit block or allow for hooks that make permission decisions
hookSpecificOutputPer-event structured output (see below)

Asynchronous response (hook delivers result later):

{ async: true, asyncTimeout: 30 }

The hookSpecificOutput field carries per-event capabilities. For PreToolUse hooks, it can include:

  • updatedInput: modified tool arguments (the tool runs with these args instead of the originals)
  • permissionDecision: an explicit allow/block decision
  • additionalContext: extra context injected into the tool execution environment

For PostToolUse hooks, it can include:

  • updatedMCPToolOutput: a replacement for the tool's output

This is how hooks achieve surgical control. A PreToolUse hook can rewrite tool arguments (normalizing paths, sanitizing inputs, or transforming the request) without owning tool execution. A PostToolUse hook can transform or augment the tool output without re-running the tool.

Exit code semantics for command hooks:

# Exit code 2 from a command hook = blocking result
# stderr goes to the model as context, and the tool call is blocked
exit 2

# JSON on stdout from a command hook = rich structured response
# The runner parses this and maps it to the response protocol
stdout: |
  {
    "continue": false,
    "stopReason": "Security policy violation: destructive operation detected",
    "decision": "block"
  }

# Exit code 0 with no stdout = success, no blocking, no message

Error Isolation and Aggregation

Hook failures are isolated. A hook that crashes, times out, or produces an error never crashes the main agent loop. The failure is captured as one of four outcomes:

  • success: hook ran, returned a result, no issues
  • blocking: hook explicitly blocked execution (exit code 2 for command hooks, or "continue": false in JSON response)
  • non_blocking_error: hook failed (exception, timeout, non-zero exit that isn't 2) but execution continues. Error is logged to the transcript.
  • cancelled: hook was aborted (parent operation cancelled, or the hook's own timeout expired)

When multiple hooks are registered for the same event, their results are aggregated:

aggregate_hook_results(results):
  # Any blocking result blocks: order doesn't matter
  blocking_results = [r for r in results if r.outcome == "blocking"]
  if blocking_results:
    return combined_blocking_error(blocking_results)

  # Non-blocking errors are logged but don't stop execution
  errors = [r for r in results if r.outcome == "non_blocking_error"]
  if errors:
    log_errors_to_transcript(errors)
    # execution continues past this point

  return success(aggregate_context(results))

A blocking result from any single hook blocks the entire operation, regardless of what other hooks returned. Multiple non-blocking errors are all logged. This is the right aggregation policy: a blocking hook is a veto, not a vote. You don't want "mostly approved" to mean "proceed."

The error isolation guarantee means hooks are safe to add without risk of destabilizing the main loop. A hook that occasionally fails with a non-blocking error is not a production incident. It logs, and execution continues. This safety property is what makes it reasonable to put prompt and agent hooks in production paths where the underlying LLM might occasionally time out.

Safety: SSRF and Execution Boundaries

HTTP hooks require SSRF protection. Without it, a misconfigured hook could be weaponized to make the agent probe your internal network. The protection blocks requests to private IP ranges (RFC 1918: 10.x.x.x, 172.16-31.x.x, 192.168.x.x), cloud metadata endpoints (169.254.x.x), and CGNAT ranges. Loopback addresses (127.x.x.x, ::1) are intentionally allowed because http://localhost/ is the primary use case for local audit servers and policy proxies.

The validation happens at DNS resolution time, not connection time. This prevents DNS rebinding attacks: a hostname that resolves to a public IP at validation time but to a private IP at connection time. IPv4-mapped IPv6 addresses are checked alongside IPv4 to prevent bypass via hex notation.

Agent hooks are sandboxed sub-agents. They run a full multi-turn loop with access to codebase tools, but with two constraints: they cannot spawn additional sub-agents, and they must return their result via a StructuredOutput tool call. This enforces that agent hooks terminate with an explicit result rather than running indefinitely. The sub-agent has a 50-turn cap. If it exhausts turns without calling StructuredOutput, the hook returns outcome: 'cancelled' silently (no error shown to the user, execution continues).

Prompt hooks use the direct LLM query path rather than the user-input processing path. This is deliberate: if a UserPromptSubmit hook internally called into the user-input processing pipeline, it would trigger another UserPromptSubmit hook, creating infinite recursion. Prompt hooks bypass this by querying the LLM directly, skipping the hook dispatch mechanism.

Production Considerations

1. Hook execution order is not guaranteed for concurrent async hooks.

Hooks on the same event run in configuration order for synchronous execution. But when multiple hooks use async: true, they complete in arbitrary order. Design each hook to be fully independent. Don't write hook A that reads state set by hook B, even if hook B appears first in the configuration. The aggregation is commutative, not sequential.

2. Agent hooks silently cancel on the 50-turn limit.

An agent hook that doesn't reach a conclusion within 50 turns returns outcome: 'cancelled' with no error surfaced to the user. From the user's perspective, the hook simply didn't run. Write agent hook prompts that direct minimal, targeted exploration: "Verify the unit tests pass by reading the most recent test output file." If the goal requires open-ended reasoning without a clear termination path, use a command or prompt hook instead.

3. Three registration sources contribute to the same unified configuration.

Hooks can be registered from settings files (user-global, project-local, or session-local), from plugin callbacks (code-backed hooks registered programmatically), and from frontmatter or skill hooks (declared in skill configuration files). All three sources merge into the same unified hooks configuration structure and run through the same executor. There is no separate "plugin hook" runtime. Everything goes through the same path.

4. The once flag runs a hook exactly once, then deregisters it.

Some initialization scenarios require a hook that runs once and never again. For example, seeding a database on the first SessionStart. The once flag handles this: the hook fires, then removes itself from the registry. This is a registration pattern, not an ordering mechanism. Don't use once to control execution order between two hooks. If hook A must run before hook B on the same event, that's a different problem and requires a different approach.

5. SSRF protection is bypassed when a proxy mediates DNS resolution.

When a global network proxy is in use, the SSRF guard validates the proxy's IP address (typically a publicly-routable, allowed address) rather than the final destination. The guard cannot see through the proxy. In proxy-mediated or sandboxed environments, apply network-level controls (proxy allowlists, firewall rules) rather than relying solely on the application-layer SSRF guard.

Best Practices

  • Do use typed conditions, not hook-internal filtering. The if condition is evaluated before the hook executor is spawned. A non-matching hook costs nothing. An unfiltered hook that inspects input inside the hook logic still costs a subprocess or LLM call for every invocation.

  • Do match the execution mode to the problem. Use command for deterministic validation (scripts, linters, formatters). Use prompt for classification and intent checks (one LLM call, fast). Use agent only for complex verification that genuinely requires exploration. Use http for external audit and webhook delivery.

  • Do use async: true for observability and audit hooks. Hooks that log, audit, or notify external systems don't need to block the agent. Fire-and-forget hooks add zero latency to the critical path.

  • Don't write hooks that depend on other hooks' side effects on the same event. The aggregation is parallel and order-independent. Any hook that assumes another hook's state was applied first will fail intermittently in ways that are hard to reproduce.

  • Don't use agent hooks for ambiguous or open-ended goals. A vaguely-specified agent hook prompt that says "verify the changes look good" will exhaust its turn budget reasoning in circles and silently cancel. Be specific: name the exact verification step, the file to read, and the condition to check.

  • Do register the analytics and audit hooks with async: true. The production pattern is: blocking hooks for safety and validation (command or prompt), fire-and-forget hooks for observability and audit (http or command with async).

  • Don't rely on hook execution order for correctness. If two hooks on the same event produce conflicting results, the aggregation will combine them. It won't pick one and ignore the other based on position.

  • Tool System: Hooks extend tool dispatch at the PreToolUse and PostToolUse events. Understanding the tool lifecycle makes hook timing clearer.
  • Safety and Permissions: The if condition syntax in hooks reuses the same pattern-matching evaluator as permission rules, so the mental model transfers directly.
  • Command and Plugin Systems: Hooks and commands are complementary extension mechanisms: commands are user-invoked, hooks are event-driven. Both extend behavior without modifying the core loop.
  • MCP Integration: MCP tools are hookable via the same PreToolUse and PostToolUse events as built-in tools. The Elicitation and ElicitationResult events are specific to MCP server interactions.
  • Observability and Debugging: Hook outcomes are logged as first-class events in the observability layer. Hook spans appear in session tracing, composing with the debugging tools to give complete visibility into hook behavior and timing.
  • Pattern Index: All patterns from this page in one searchable list, with context tags and links back to the originating section.
  • Glossary: Definitions for domain terms used on this page.