Skip to content

Hooks ​

A hook subscribes to selected session events and can run a side effect after each matching event is recorded. Hooks run in the serving process for every session of the agent. They cannot change the turn, the prompt, or the reply. A handler that throws is logged and skipped.

Treat the event as read-only. Later subscribers see the same object. A hook is the wrong surface for anything that must run before the model or must fail a turn. See Hook boundaries.

defineHook is unrelated to Cursor Agent hooks, the .cursor/hooks.json scripts that can observe, block, or modify the agent loop. Those still run inside a local session workspace.

Author a hook ​

Author bot/hooks/<name>.ts with defineHook from @grok/agent-sdk/hooks. This hook meters tokens and pages on failure:

ts
// bot/hooks/usage.ts
import { metrics } from "@opentelemetry/api";
import { defineHook } from "@grok/agent-sdk/hooks";

const meter = metrics.getMeter("acme");
const inputTokens = meter.createCounter("acme.tokens.input");
const turnFailed = meter.createCounter("acme.turn.failed");

export default defineHook({
  events: {
    async "turn.completed"(event, ctx) {
      if (ctx.session.purpose === "eval" || event.data.usage === undefined) {
        return;
      }
      inputTokens.add(event.data.usage.inputTokens);
    },
    async "turn.failed"(event, ctx) {
      const pagerUrl = process.env.PAGER_WEBHOOK_URL;
      if (
        pagerUrl === undefined ||
        ctx.session.purpose === "eval" ||
        event.data.status === "cancelled"
      ) {
        return;
      }
      turnFailed.add(1, { channel: ctx.channel.id });
      await fetch(pagerUrl, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          agent: ctx.agent.name,
          session: ctx.session.id,
          message: event.data.message,
        }),
        signal: AbortSignal.timeout(5_000),
      });
    },
  },
});

Any module under bot/hooks/ is a hook named by its path without the extension. *.test.ts and *.spec.ts are skipped. The default export must be defineHook(...), names can't contain __, and an empty events map is skipped with a warning. agent-sdk validate reports all three. An extension mounts its hooks as <ns>__<name>, and disableHook() removes one. See Adjust a mounted extension.

agent-sdk init scaffolds bot/hooks/memory.ts. Delete it to opt out of the journal. See Memory.

Read hosted secrets inside the handler, not at module scope. See Add egress and secrets. Give outbound calls a timeout, because a stalled request blocks later handlers on that session.

Events and payloads ​

Keys are event types from the event vocabulary, or "*" for every event. A typed key narrows event.data, a "*" handler receives the union so you switch on event.type, and every event carries { type, index, sessionId, turnId?, at, data }. Skip *.appended deltas when you want the final text.

Eventevent.data
message.received{ text }
turn.completed{ result?, usage?, cost? }. usage has inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, and optional reasoningTokens. cost has totalUsd and the model it was priced against
turn.failed{ message, status? }. status is "error" or "cancelled" when present
actions.requested{ calls: [{ callId, toolName, args?, parentCallId? }], parentCallId? }. One envelope per model step. parentCallId marks subagent work
actions.completed{ results: [{ callId, toolName, output?, isError, stubbed?, parentCallId? }], parentCallId? }. Emitted when every call in that batch has a result
action.result{ callId, toolName, output?, isError, stubbed?, parentCallId? }. Live per-call envelope. stubbed means a dry-run session answered a write without running it
action.tool_retry{ callId, toolName, attempt, reason, connection? }. A read is running again after a transient failure. attempt is the try about to run

The types are SessionEvent, SessionEventType, and HookContext, exported from @grok/agent-sdk.

Handler context ​

MemberWhat it is
ctx.sessionRead-only session info: id, channelId, mode (chat or task), purpose (live or eval), auth, plus title and sdkAgentId when set
ctx.agent{ name } of the agent the event belongs to
ctx.channel{ id, continuationToken }. The token is null when the session can't take follow-ups
ctx.host.kvDurable JSON, shared by every session of the agent. Prefix keys with ctx.session.id for per-session state
ctx.host.filesDurable files, bound to this session. Pass { scope: "deployment" } for agent-wide files
ctx.host.mcp, ctx.host.github, ctx.host.slackThe same shared clients tools get
ctx.host.remindersPer-session reminders
ctx.artifactsSession-bound artifacts facade. tag fills in sessionId and turnId
ctx.stateRootAbsolute path of the local state root. It resets when a hosted deployment is replaced. Keep derived state in kv or files

Hook dispatch ​

A hook runs after the event is recorded. It never delays the model.

RuleWhat happens
Same sessionEvents dispatch one at a time
Eval sessionsThe same stream fires. Skip metering or paging when ctx.session.purpose === "eval"
Host restartRecorded events are not replayed into hooks, so a mirror needs no dedupe
Slow handlerHolds the next event's handlers on that session, not the model. Keep handlers short and queue anything slow

Hook boundaries ​

You want toUse instead
Add context before the model runsThe channel's send message and workspaceFiles, instructions.md, skills, or sandbox/workspace/ seed files
Reply on Slack, comment on a PR, or post any other deliveryThe channel's events map, or the Slack and GitHub packs
Show PR progress (merge-box check, sticky banner)githubChannel({ progress: { commitStatus, banner } }). See GitHub agents
Gate a change on behaviorEvals
Observe every session for audit, metrics, or a transcript mirrorA hook

To act on a turn's decision, such as a label or a ticket, have the model record it with an authored tool. A turn.completed hook then reads the recorded decision and acts on it through ctx.host.

Test a hook ​

A hook definition is a plain object, so a unit test calls hook.events["turn.completed"] directly with an event and a stub HookContext. Discovery skips *.test.ts, so the test can live next to the hook.

CommandWhat it reports
agent-sdk validate --dir .Discovery errors and the empty-handlers warning
agent-sdk info --dir . --jsonLoaded hooks under agents[].hooks
agent-sdk run --dir . --message "…"Serve log on stderr, including hook "<name>" handler for <event> threw: …

On hosting, read the same log with agent-sdk logs.

See also Sessions, OpenTelemetry, and Channels.