Hooks
Lifecycle hooks for plugins — pre_tool_call veto, post_assistant_reply, and 6 others.
Canonical contract:
packages/sdk/docs.md—HookNameenum,Plugindiscriminated union.
Hooks are typed extension points in the agent loop. Plugins register handlers; the loop invokes them at fixed lifecycle moments. The list of hook names is closed (8 fixed hooks, ADR D100) — adding a new one is a SDK release, not a runtime concern.
The 8 hooks
| Hook | When | Can veto? |
|---|---|---|
pre_user_send | Before user prompt added to history | No |
post_user_send | After user prompt added | No |
pre_tool_call | Before agent invokes a tool | Yes (return { block: true, message? }) |
post_tool_call | After tool returns | No |
pre_assistant_reply | Before model generates reply | No |
post_assistant_reply | After model emits final assistant message | No |
pre_dispose | Before agent disposes | No |
post_dispose | After agent disposed | No |
Veto pattern
pre_tool_call is the only veto hook. Returning { block: true, message: "reason" } aborts the tool call — the agent receives a synthetic tool_result isError: true and continues. Never throw from a veto hook (ADR D101).
import { definePlugin } from "@theokit/sdk";
const noShellInProd = definePlugin({
kind: "hook",
name: "no-shell-in-prod",
hooks: {
pre_tool_call: (ctx) => {
if (ctx.toolName === "shell" && process.env.NODE_ENV === "production") {
return { block: true, message: "Shell tool disabled in production." };
}
},
},
});
const agent = await Agent.create({ apiKey, model, plugins: [noShellInProd] });Plugin context
Every hook receives a typed context object. The shape depends on the hook. In dev mode the context is sealed via Proxy (ADR D99) — assigning to it throws, helping you catch accidental mutation.
Example PreToolCallContext:
type PreToolCallContext = {
toolName: string;
toolArgs: unknown;
agentId: string;
runId: string;
turnIndex: number;
// ...
};See the Plugins concept for the full Plugin contract.