Appearance
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:
| Member | What it is |
|---|---|
ctx.session | Read-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.stateRoot | The agent's durable state root. Prefer ctx.host.kv / ctx.host.files for derived state; this tree resets on hosted replace |
ctx.host | Shared 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.artifacts | Session-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:
| Hooks | Channel events | Evals | A/B (defineAB) | |
|---|---|---|---|---|
| Scope | every session on the agent | sessions the channel owns | one test turn | every live session; enrollment at creation, metrics on each turn |
| Job | observe: audit, metrics, mirrors, derived state | deliver: replies back to the channel's surface | assert: gates over the trajectory | ab.assigned + fold stream → onSample |
| Can affect the run | no | yes, it owns the surface | n/a | yes through arm instructions or session.abs; collection is observe-only |
| Authored at | agent/hooks/*.ts | channel config | evals/**/*.eval.ts | agent/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:
- Sessions and streaming: every event a hook can see
- OpenTelemetry: OTLP traces and metrics from the same event stream
- Deployment: runtime logs and export paths
- Channels: the delivery-side counterpart
- Live A/B metrics: sticky variants over the same event stream