Skip to main content

Hook Registry

The hook registry lets you intercept and modify agent behaviour at specific points in the turn cycle without modifying core code. There are three execution models, each suited to a different kind of hook.

Execution models

ModelMethodExecutionFailureUse for
VoidfireVoidAll handlers in parallel (Promise.allSettled)Swallowed (fail-open)Logging, analytics, notifications
ModifyingfireModifyingSequential, results mergedPropagatedAmend prompts, override args
ClaimingfireClaimingSequential, stops at first { handled: true }PropagatedRouting decisions, blocking

Hook points in the turn cycle

Hook pointModelWhen it fires
session_startVoidBefore the turn begins
before_prompt_buildModifyingAfter memory prefetch, before LLM call
before_tool_callClaimingBefore each tool executes
after_tool_callVoidAfter each tool executes
agent_doneVoidAfter the full turn completes

Registering a hook

All register*() methods return a cleanup function — call it to deregister:

const unhook = hookRegistry.registerVoid('agent_done', async (ctx) => {
await analytics.track('turn_completed', {
sessionId: ctx.sessionId,
turnCount: ctx.turnCount,
})
})

// Later, when cleaning up:
unhook()

Void hooks

All void handlers run in parallel. If one throws, the others still complete and the error is swallowed. Safe for any side effect that must not abort the agent.

hookRegistry.registerVoid('session_start', async (ctx) => {
console.log(`Session started: ${ctx.sessionId}`)
})

Modifying hooks

Handlers run sequentially. Each can return a partial object; results are merged — first non-null value per key wins. Later handlers cannot override an earlier handler's value.

hookRegistry.registerModifying('before_prompt_build', async (ctx) => {
// Inject additional context into the system prompt
return {
additionalContext: `Today is ${new Date().toDateString()}.`,
}
})

Claiming hooks

Handlers run sequentially and stop as soon as one returns { handled: true }. Designed for routing: the first handler that claims the input wins.

// Dangerous command gate — rejects shell commands that match known destructive patterns
hookRegistry.registerClaiming('before_tool_call', async (ctx) => {
if (ctx.toolName !== 'terminal') return { handled: false }

const dangerous = isDangerousCommand(ctx.args.command)
if (!dangerous) return { handled: false }

return {
handled: true,
error: `Blocked: '${ctx.args.command}' matches a dangerous command pattern.`,
}
})

:::warning Tool rejection requires a matching tool_result If a before_tool_call hook rejects a tool, you must still persist an error tool_result for the rejected call. Anthropic requires a tool_result for every tool_use in the preceding assistant message — even blocked ones. The core handles this automatically when hooks return { handled: true, error: '...' }. :::