Skip to content

Patterns

This page covers common patterns for implementing hooks effectively. These patterns apply across runtimes, though syntax varies.

Gates vs Observers

Hooks come in two kinds: gates block actions before they happen, observers watch actions after they complete.

Gates execute on pre-events (PreToolUse, UserPromptSubmit). They must respond quickly because they block the agent. A gate returns an allow/block decision and can explain the block to the model. Use gates for enforcement: blocking writes to protected paths, rejecting dangerous commands, validating arguments.

User Request → Gate Hook → Allowed? → [Tool Execution]
                              ↓ No
                          [Blocked]

Observers execute on post-events (PostToolUse, SessionEnd). They cannot change what happened but can run asynchronously. Use observers for logging, metrics, and alerting - recording file modifications, timing command execution, building audit trails.

User Request → [Tool Execution] → Observer Hook → [Logging/Metrics]

Exit Code Semantics

Hooks communicate decisions through exit codes. The standard convention:

Exit Code Meaning Effect
0 Allow Action proceeds normally
1 Warn Action proceeds, warning logged
2+ Block Action prevented, message shown to model

This convention allows graduated responses. A hook might warn about a suspicious pattern while still allowing it, or block definitively when rules are violated.

Feedback on Blocks

When blocking, hooks should write actionable feedback to stdout. This message is typically shown to the model, allowing it to adjust:

Exit code: 2
Stdout: "Cannot write to /etc: path is protected. Consider writing to project directory instead."

Good feedback helps the model self-correct rather than repeatedly hitting the same block.

Enforcement Strategies

Allowlist

Only permit explicitly approved actions. Everything else is blocked.

ALLOWED_COMMANDS = ["npm test", "npm run build", "git status"]

if command not in ALLOWED_COMMANDS:
    block("Command not in allowlist")

Allowlists give maximum security but require maintenance when legitimate commands change. Use in production or high-security environments.

Blocklist

Permit everything except explicitly denied actions.

BLOCKED_PATTERNS = ["rm -rf", "sudo", "> /dev/"]

if any(pattern in command for pattern in BLOCKED_PATTERNS):
    block("Command matches blocked pattern")

Blocklists are permissive by default with lower maintenance, but you must anticipate every dangerous pattern. Use during development or exploratory work.

Context-Aware

Decisions based on context, not just the action itself.

if tool == "write" and path.endswith(".py"):
    if not has_tests(path):
        warn("Writing Python file without corresponding tests")

Context-aware enforcement gives nuanced decisions with fewer false positives, but the logic is more complex and may have gaps. Use for workflow enforcement or code quality gates.

Composition

Multiple hooks can handle the same event. Hook ordering matters because of how they compose.

Sequential Execution

Hooks execute in order. Each hook sees the (potentially modified) result of previous hooks.

Hook A → Hook B → Hook C → Tool Execution

If Hook A modifies input, Hook B sees the modified version.

First-Block-Wins

For gate hooks, the first hook to block stops execution. Subsequent hooks don't run.

Hook A: Allow
Hook B: Block ← Execution stops here
Hook C: (never runs)

This means hook ordering matters. Security-critical hooks should run first.

All-Must-Pass

Some systems require all hooks to allow for action to proceed:

Hook A: Allow
Hook B: Allow
Hook C: Allow
─────────────
Result: Allow (all passed)

Combining Gates and Observers

A common pattern combines gate hooks (for enforcement) with observer hooks (for logging):

Security Gate → Audit Observer → Tool Execution → Result Observer

The gate blocks violations. The observers log everything for audit, regardless of gate decisions.

Stateless Design

Hooks should be stateless - each invocation is independent with no memory of previous calls.

Hooks may run in separate processes, so they can't share memory. Session state belongs in the agent, not hooks. Stateless hooks are easier to test, and parallel execution is safe when no shared state exists.

If you need state, use external storage (files, databases), design for concurrent access, and accept eventual consistency. See the State Assumptions anti-pattern below.

Performance Considerations

Hooks add latency to every intercepted action. Design accordingly:

Keep Hooks Fast

Avoid network calls in gate hooks - they block the agent. Cache expensive computations. Use matchers to skip irrelevant events. Profile any hook that runs frequently.

Async When Possible

Observer hooks that don't affect execution can run asynchronously:

Tool Execution → Fire Observer Async → Continue
                   [Background logging]

This keeps the agent responsive while still capturing telemetry.

Batch Operations

For high-frequency events, consider batching:

Events: 1, 2, 3, 4, 5
        └────┬────┘
     Batch every 100ms
       Process batch

Batching trades latency for throughput in observer scenarios.

Anti-Patterns

Miscalibrated Gates

Too aggressive and the agent can't complete legitimate tasks. Too silent and users get confused by unexplained blocks. Start permissive and add blocks for specific violations as you discover them. Always explain blocks - if a hook rejects an action, say why.

Expensive Gates

Slow hooks on critical paths make the agent feel sluggish. Users disable slow hooks. Profile your gate hooks, move expensive logic to observers, and cache where possible.

State Assumptions

Assuming hooks remember previous invocations causes intermittent failures and race conditions. Hooks run in separate processes and don't share memory:

# This doesn't work - file_count resets each invocation
file_count = 0

def on_file_write():
    global file_count
    file_count += 1  # Always 1

If you need state, read from external storage and design for concurrent access.

Security by Obscurity

Hooks are enforcement, not secrets. Don't rely on the model "not knowing about" your restrictions - a determined prompt will find the boundaries through trial and error. Design hooks to work even when the model actively tries to bypass them.