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.
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.
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.
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.
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:
Combining Gates and Observers¶
A common pattern combines gate hooks (for enforcement) with observer hooks (for logging):
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:
This keeps the agent responsive while still capturing telemetry.
Batch Operations¶
For high-frequency events, consider batching:
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.