Skip to main content

Command Palette

Search for a command to run...

Agent SDK

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:

// bot/hooks/usage.tsimport { 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.