Skip to content

Hooks

A hook is an observe-only subscriber to the session event stream. Hooks run after each event is recorded and fanned out (file persistence flushes in the background). That makes them the home for audit logging, metrics, mirroring transcripts into your own store, and maintaining derived state. Handler errors are logged and never fatal. A hook can't modify events, inject context into the next turn, or block a turn.

For deterministic context composition before the model runs, use the host path that already owns the wake: channel handlers (fetch, callTool, workspaceFiles, and the message you pass to send), plus instructions.md, skills, and sandbox/workspace/ seed files. Hooks observe what happened; they do not assemble the prompt.

Author agent/hooks/<name>.ts with defineHook from @cursor/july/hooks:

ts
import { defineHook } from "@cursor/july/hooks";

export default defineHook({
  events: {
    async "turn.completed"(event, ctx) {
      const prior = await ctx.host.kv.get("last-result");
      const notes = await ctx.host.files.read("notes.md");
      console.log("turn done", ctx.session.id, event.data.usage, prior, notes);
    },
    async "turn.failed"(event, ctx) {
      // page, count, or record
    },
  },
});

Keys are event types (the full list is in the event vocabulary), or "*" for everything. Handlers receive the event with its envelope (index, sessionId, turnId?, at) and a HookContext:

MemberWhat it is
ctx.sessionRead-only session info: id, channel, mode, auth
ctx.agent{ name } of the agent the event belongs to
ctx.channel{ id, continuationToken } for the owning channel
ctx.stateRootThe agent's durable state root. Prefer ctx.host.kv / ctx.host.files for derived state; this tree resets on hosted replace
ctx.hostShared host services; same as a tool's ctx.host. Pull JSON with ctx.host.kv and file-shaped state with ctx.host.files (session-bound by default; pass { scope: "deployment" } for agent-wide files)
ctx.artifactsSession-bound artifacts facade: tag auto-fills the session

Hook context includes ctx.host, the same shared services a tool gets. Persist JSON with ctx.host.kv and file-shaped state with ctx.host.files. Hooks observe; they do not own delivery surfaces.

Hooks, channel events, evals, or A/B?

All of them consume the same stream, for different jobs:

HooksChannel eventsEvalsA/B (defineAB)
Scopeevery session on the agentsessions the channel ownsone test turnevery live session; enrollment at creation, metrics on each turn
Jobobserve: audit, metrics, mirrors, derived statedeliver: replies back to the channel's surfaceassert: gates over the trajectoryab.assigned + fold stream → onSample
Can affect the runnoyes, it owns the surfacen/ayes through arm instructions or session.abs; collection is observe-only
Authored atagent/hooks/*.tschannel configevals/**/*.eval.tsagent/ab.ts or agent/ab/*.ts

For GitHub merge-box checks and sticky PR banners, use githubChannel({ progress: { commitStatus, banner } }) from @cursor/july/channels/github. That is the supported Autofix-style path. See GitHub: Show PR progress. Override channel events only when the lifecycle is custom (for example Approval Buddy's never-red status from tool output). Do not use defineHook for those writes.

Patterns

Usage metering: subscribe to turn.completed and forward event.data.usage (token counts) to your metrics system.

Failure alerting: turn.failed carries the message, and ctx.session.id points at the trace.

Derived state: agent.bound fires when the Cursor SDK agent id is known (bc-… on cloud). A PR agent can record PR → agent id from it in a hook with ctx.host.kv, so later webhook wakes resume the same cloud conversation. Prefer ctx.host.kv or ctx.host.files for ids that must survive hosted replace. stateRoot resets on replace.

Transcript export: subscribe to "*" and append to your own store. The NDJSON envelope is already ordered and replayable.

What's next

Continue with these pages: