Skip to main content
A hook is an object { name, preToolCall?, postToolCall?, preGenerate?, postGenerate? } that runs inside the agent loop, around every tool call and every model call. Use one to write an audit log, deny a tool, redact or truncate a result, inject context before the model call, or change the request (a header, temperature) it sends. Hooks are code you write, not policy and not a human decision: for rules that allow, deny or ask per tool, and for a person approving a call, use permissions and needsApproval (Approvals). Listeners such as onEvent only watch a run and cannot change it (Streaming); a hook can.

Your first hook

Pass hooks to createAgent({ hooks }). This one logs every tool call and what it returned:
Every method is optional: implement only the points you need. A hook that returns nothing only watches; mutating ctx.args, ctx.messages or ctx.request in place also changes the run.

The four hook points

The common fields on every context are agentId, agentName, sessionId, messages (the live conversation history), metadata (a free-form bag) and subagent (set inside a sub-agent). ctx.emit exists only in streamed runs and adds compaction.start / compaction.done events to the stream.

Hook outcomes

A tool-call hook can change what happens, not just watch it. A preToolCall hook may return:
  • nothing: the call goes on unchanged (mutating ctx.args in place still works).
  • { deny: reason }: the call does not run. The model gets the same kind: 'denied' tool error a needsApproval deny produces (see Approvals), streams see tool.error, and onPermissionDecision records decision: 'deny' with hook and reason.
  • { result: value }: the call does not run and value is its result. The tool.done event and the transcript’s tool message (metadata) carry replacedByHook: '<hook name>'.
  • { input: args }: the call runs with args. They are validated against the tool’s input schema again; a mismatch becomes a kind: 'validation' tool error whose message names the hook, and the tool does not run.
A postToolCall hook may return { result: value } to replace the result the model sees (to redact or truncate it); nothing keeps it. Hooks run in registration order: the first deny or result of a pre-hook skips the pre-hooks after it, inputs chain (each hook sees the previous one’s as ctx.args), and each post-hook sees the result an earlier one replaced. A hook that throws still rejects the run, as before. Sub-agents inherit the lead agent’s hooks and their outcomes.

Recipes

Each recipe is a complete program. They run against mockModel, so none of them needs an API key or a live model call; the same pattern tests your own hooks offline (Testing). Each one scripts a model turn that calls a tool, then a final answer.

Deny a tool by name

Add a default argument

Return { input } to run the tool with different arguments. They are validated against the tool’s schema again.

Truncate or redact a result

postToolCall receives the settled result and may return { result } to replace what the model sees. The tool itself ran unchanged.

Inject context before a model call

preGenerate sees the live ctx.messages and ctx.request. Push a message to add context, or change a request field.

Order and interaction

For one tool call, the order is: argument validation, preToolCall hooks, permission rules (permissions), tool guardrails, the tool’s needsApproval, the approval pause, execution, postToolCall hooks. Hooks run before every approval decision, so rules, needsApproval and the human all see (and approve) the input a hook produced; the pending approval’s args show it. When an approved call is resumed, the pre-hooks run again: they may still deny it or supply its result, but an { input } that differs from the approved input is refused with a tool error instead of running. See Approvals for the pause and the resume.
  • Several hooks run in the order of the hooks array, one at a time: each is awaited before the next, so a later hook sees what an earlier one changed.
  • A hook that throws still rejects the run. Hooks are trusted code: the error is not turned into a tool result for the model.
  • Compaction. createAgent({ hooks }) runs your hooks first and the compaction hook after them, so compaction sees what they added (Context compaction).
  • Sub-agents inherit the lead agent’s hooks. ctx.subagent is set when a hook runs inside one, so a hook can return early to see only the top-level run (Hooks inside sub-agents).

HookRegistry

createAgent({ hooks }) takes a plain array. Code that uses the executor API (The executor API) passes a HookRegistry: an ordered collection with register, registerMany, get, has, list, unregister, clear and size. Registering a second hook with the same name replaces the first and logs a warning.
HookRegistry, AgentHook and the context types are available from the package root and from the @lousho/build-ai-agent/hooks subpath. Hooks authored in the Agent Forge canvas editor are compiled into a HookRegistry and run sandboxed rather than in the host process: see Agent Forge.