Sweep 19 Sep 2026 · 02:36Z Build v2.1.278 500 read Stable v2.1.267 Latest v2.1.278 Next v2.1.278 Feeds RSS JSON llms.txt Unofficial
Mods API · 02 of 03

Events

Every event a hook can be registered for, with the payload it arrives with and the result it may return.

Events71on this page
Mined from2.1.278the shipped binary

An event is a name a hook can be registered for. When it fires your function is handed the payload below, along with the next it may pass control to, and what it returns has to be the result type on the same row or the runtime treats the hook as having said nothing.

The declarations keep these as two maps, one from event name to payload and one from event name to result. They are joined here by name, because somebody reading about tool.pre wants both halves at once. The map each group came from is on its heading, so a name can be traced back to the declaration it was mined from.

EngineEventOf # 38 events

Results come from EngineResultOf.

tool.call #

Payload
ToolCallInput
Result
ToolCallResult

Fires when the engine is about to run a tool. `next(e)` runs the hooks beneath, then core (the permission prompt, the tool itself).

Return `{ deny: reason }` to refuse or `{ result }` to answer yourself; a hook that returns while its `next` is pending aborts what runs beneath. The managed-settings hooks run first: their deny is the call's result.

tool.check #

Payload
ToolCheckInput
Result
ToolCheckResult

Fires when the engine decides whether a tool call may run, after the `tool.call` and PreToolUse hooks and before the mode settles an ask.

`next(e)` resolves to the engine's verdict (rules, mode, the tool's own check, PreToolUse's decision); return any `{ decision }`. `$.tool.check` runs the same chain and executes nothing.

on("tool.check", { tool: "Read" }, () => ({ decision: "allow" }))

ui.render #

Payload
RenderInput
Result
RenderElement

Fires when the engine is about to draw a component: once per input value (props, viewport width), plugin load or `$.ui.invalidate("ui.render")`.

A repaint reuses the answer; a clock invalidates. `next(e)` resolves to the drawing: return it, wrap it, draw your own, or rewrite `props`. A tree that does not validate draws the engine's own; `--plugin-dir` is told why.

ui.resolve #

Payload
ResolveInput
Result
ElementTable

Fires when the plugins load (not per draw), once per surface, component and plugin: `e` names the surface and component, never the props.

`next(e)` resolves to the surface's table, which `$.ui.resolve(e)` then reads. Return it, one with an element restyled for every other plugin, or one with a key left out (a fragment there); own table: hook skipped.

ui.press #

Payload
UiPressArgument
Result
UiPressResult

Fires when a `Button` a render hook drew is pressed on a surface; `e` is `{ plugin, element, component, surface }`, `element` the button's `key`.

`next(e)` runs the hooks beneath, then core: the element's own `onPress` closure, in its plugin's environment, resolving to `{ element }`. Return `next(e)` to let the press through, or `{ element }` to take it.

ui.input #

Payload
UiInputArgument
Result
UiInputResult

Fires when an `Input` a render hook drew changes or is submitted; `e` is `{ plugin, element, component, surface, kind, value }`.

`next(e)` runs the hooks beneath, then the element's own `onInput` or `onSubmit` with `e.value` as the chain left it, resolving to `{ element, value }`; `next({ ...e, value })` rewrites the typing, an answer takes it.

ui.select #

Payload
UiSelectArgument
Result
UiSelectResult

Fires when a `Select` a render hook drew is picked from; `e` is `{ plugin, element, component, surface, value }`.

`next(e)` runs the hooks beneath, then the element's own `onSelect` with `e.value` as the chain left it, resolving to `{ element, value }`; `next({ ...e, value })` rewrites the pick, an answer takes it.

ui.message #

Payload
UiMessageArgument
Result
UiMessageResult

Fires when a `Client` THIS plugin drew posts from its surface module (`surface.post(data)`); only this plugin's hooks see it.

`next.origin` names `client`: `data` came from code. Core answers `{}`; `next({ ...e, data })` rewrites the data; `{ props }` hands the posting instance its next props without a redraw. One per instance per frame.

ui.scroll #

Payload
UiScrollInput
Result
UiScrollResult

Fires before a site's window moves: the person's wheel or scroll keys on a `Pane` body or the `AbovePrompt` band, at its edges too; `$.ui.scroll`.

`next(e)` moves it to `e.offset` and draws: `{}`; `next({ ...e, offset })` elsewhere; no `next` (`{}` or `{ deny }`) leaves it undrawn, so a hook drawing its own rows under a header moves them by `e.by` and invalidates.

on("ui.scroll", { requestId: "log" }, ($, e) => (scrollOwnRows(e.by), {}))

ui.focus #

Payload
UiFocusInput
Result
UiFocusResult

Fires before a site's focus ring moves: the person's Tab, arrows or click in a `Pane` or the band; an `autoFocus` element taking it; `$.ui.focus`.

`next(e)` lands it on `e.element` (absent: one of the engine's stops) and draws: `{}`; `next({ ...e, element })` on another of `e.plugin`'s; no `next` (`{}` or `{ deny }`) keeps it where it was, drawn as it was.

on("ui.focus", { requestId: "list" }, ($, e, next) => (mark(e), next(e)))

agent.offer #

Payload
AgentOfferInput
Result
AgentOfferResult

Fires when the engine offers an agent type to the model, in the agent listing and again at dispatch; `next(e)` resolves to `{ isOffered: true }`.

Return `{ isOffered: false }` to keep the type out of the listing and refuse its dispatch. A hook that fails passes it through.

on("agent.offer", { agent: "Plan" }, () => ({ isOffered: false }))

agent.spawn #

Payload
AgentSpawnInput
Result
AgentSpawnResult

Fires when the Agent tool is about to start a subagent, everything decided and its model not yet resolved.

`next(e)` resolves to `{ model }`. Return it, `next({ ...e, model })`, `{ model }` of your own (an alias resolves like the tool's parameter), or `{ deny: reason }`.

prompt.submit #

Payload
PromptSubmitInput
Result
PromptSubmitResult

Fires when a prompt is submitted, before the turn starts. `next(e)` runs the hooks beneath and the UserPromptSubmit settings hooks.

Rewrite with `next({ ...e, text })` (the user message on screen follows) or stop it with `{ drop: reason }`; a broken plugin never blocks a prompt. A prompt typed while a turn ran fires at Enter, with that turn's id.

prompt.fill #

Payload
PromptFillInput
Result
PromptFillResult

Fires when a text is about to be put in the prompt box as the person's draft (a plugin's `$.prompt.fill`); `next(e)` writes it by `e.mode`.

`replace` over the draft, `append` after it, `insert` at the cursor; rewrite `text` or `mode` going down, or answer `{ isFilled: false }` without `next` to keep it out, as core does under a dialog or headless.

on("prompt.fill", ($, e, next) => next({ ...e, text: e.text.trim() }))

prompt.suggest #

Payload
PromptSuggestInput
Result
PromptSuggestResult

Fires when a text is proposed as the prompt box's dim suggestion, Tab to take: the engine's guess after a turn, or a plugin's `$.prompt.suggest`.

`next(e)` shows it: `{ isShown }`. Rewrite with `next({ ...e, text })`, or answer `{ isShown: false }` without `next` to drop it; core answers that too while the box holds text or a turn runs.

on("prompt.suggest", { origin: { kind: "suggestion" } }, hide)

prompt.edit #

Payload
PromptEditInput
Result
PromptEditResult

Fires when the person edits the main prompt box: a key the editor took as an edit, or a paste; `next(e)` resolves the box the editor shows.

`e` is the draft before and the splice (`start`, `end`, `inputText`); a burst of keys is one edit. Rewrite `inputText` going down or the box coming up; `{ text: e.text, cursor: e.cursor }` without `next` consumes.

on("prompt.edit", ($, e, next) => next({ ...e, inputText: up(e) }))

prompt.section #

Payload
PromptSectionInput
Result
PromptSectionResult

Fires once per named section of the system prompt, when the engine assembles it; `next(e)` resolves to `{ text }` as core computed it.

Sections are cached by name for the session until `$.ui.invalidate("prompt.section")`: an unstable answer spends the model's prompt cache on every call. A hook that fails passes it through.

on("prompt.section", { name: "memory" }, () => ({ text: null }))

prompt.context #

Payload
PromptContextInput
Result
PromptContextResult

Fires once per conversation, when the engine computes the context blocks its first user message carries; `next(e)` resolves to `{ blocks }`.

Append, drop, reorder or rewrite with `next({ ...e, blocks })`; the engine renders what comes back, in order, until `$.ui.invalidate("prompt.context")` or a re-read (compaction, `/clear`).

on("prompt.context", () => ({ blocks: [] }))

prompt.attachment #

Payload
PromptAttachmentInput
Result
PromptAttachmentResult

Fires once per message the engine injects for the model on its own (a reminder, a mode transition, a mentioned file), as a request carries it.

`next(e)` resolves to `{ text }`; `{ text: null }` leaves it out. The answer holds per attachment for the process (asked again on resume or `$.ui.invalidate`); the transcript keeps the engine's record.

on("prompt.attachment", { type: "todo_reminder" }, () => ({ text: null }))

tool.describe #

Payload
ToolDescribeInput
Result
ToolDescribeResult

Fires once per tool, when the engine first renders the tool's schema in a session; `next(e)` resolves to `{ description, isDeferred? }`.

Cached for the session until `$.ui.invalidate("tool.describe")`: an unstable answer spends the model's prompt cache. An explicit `isDeferred` moves the tool behind ToolSearch (true) or into the prompt's list (false).

on("tool.describe", { tool: "Bash" }, ($, e) => ({ ...e, description }))
on("tool.describe", { tool: "Monitor" }, pin) // {...e, isDeferred: false}

command.run #

Payload
CommandRunInput
Result
CommandRunResult

Fires when a slash command is about to run (`/name args` typed, or a plugin's `$.command.run`); `next(e)` resolves to `{ text }`, its output.

Core is the engine's command (a registered one has none). Rewrite `args` with `next`, or return `{ text }` without it to answer in its place; one after `next` replaces a printed output, not a panel or prompt it opened.

on("command.run", { command: "hello" }, () => ({ text: "hello" }))

command.describe #

Payload
CommandDescribeInput
Result
CommandDescribeResult

Fires once per command, when the engine lists it for the typeahead and `/help`; `next(e)` resolves to `{ description, argumentHint, isHidden }`.

Listed answers are cached for the session until `$.ui.invalidate("command.describe")`. A hook that fails passes it through.

on("command.describe", ($, e, next) => next({ ...e, isHidden: true }))

config.set #

Payload
ConfigSetInput
Result
ConfigSetResult

Fires when a `/config` row is about to change, from the menu or a plugin's `$.config.set`; `next(e)` resolves to `{ value }` once written.

Return `{ deny: reason }` to leave the row as it is (the menu says why), or `next({ ...e, value })` to clamp it; a value of the wrong kind for the row is refused. A row a trusted source owns is core's to refuse.

on("config.set", { key: "theme" }, () => ({ deny: "the theme stays" }))

config.describe #

Payload
ConfigDescribeInput
Result
ConfigDescribeResult

Fires once per `/config` row, when the menu lists it and for `$.config.list`; `next(e)` resolves to `{ label, description, isHidden }`.

Relabel, re-describe or hide with `next({ ...e, isHidden: true })`; the answers are cached until `$.ui.invalidate("config.describe")` or the loaded plugins change. A hook that fails passes the row through.

on("config.describe", { key: "tips" }, hide) // answers isHidden: true

skill.prompt #

Payload
SkillPromptInput
Result
SkillPromptResult

Fires when the engine expands a skill's prompt for the model (`/name`, the Skill tool, a preload); `next(e)` resolves to `{ text }` as computed.

Return `{ text }` with the text the model reads instead. A hook that fails passes it through.

on("skill.prompt", { skill: "commit" }, () => ({ text: "A haiku." }))

attribution.text #

Payload
AttributionTextInput
Result
AttributionTextResult

Fires when the engine composes a git text the model is to write (`kind`: `commit`, `pr`, `exemption`, `remedy`); `next(e)` resolves to `{ text }`.

Return `{ text }` with the text the model reads instead. A hook that fails passes it through.

on("attribution.text", { kind: "commit" }, () => ({ text: "" }))

session.start #

Payload
SessionStartInput
Result
SessionStartResult

Fires once per process for each loaded plugin, before the first prompt, then once per fresh load of one (never `/clear`); `next(e)` is `{ cwd }`.

Observe. The first is awaited: a `$.tool.register` is listed by turn one. A later one runs its hooks alone: an enable, a worker respawn, or a reload (changed modules only; all if one hooks `engine.create`/`plugin.register`).

on("session.start", ($, e, next) => $.tool.register(t).then(() => next(e)))

session.receive #

Payload
SessionReceiveInput
Result
SessionReceiveResult

Fires when a delivery reaches the session (a relay's event, a peer's message, a Remote Control prompt), before it is queued; `{ text }`.

Rewrite with `next({ ...e, text })`, or return `{ consumed: reason }` to take it: nothing is queued, shown or read by the model. `origin` and `event` pass on as received; `session.send`, its dual, is reserved.

on("session.receive", { origin: "peer" }, () => ({ consumed: "muted" }))

session.compact #

Payload
SessionCompactInput
Result
SessionCompactResult

Fires when the conversation is about to be compacted (`/compact`, the threshold, a plugin, or ahead of time); `next(e)` resolves `{ messages }`.

Rewrite `instructions` or `messages` on the way down, the messages on the way up, or answer `{ messages }` of your own; `{ skip: reason }` leaves the conversation as it is. `trigger` passes on as received.

on("session.compact", { trigger: "precompute" }, () => ({ skip: "off" }))

session.attach #

Payload
SessionAttachInput
Result
SessionAttachResult

Fires when a remote client joins the session's roster of attached surfaces: it said so (ui_attach), or it first asked to draw.

Observe (a phone joined: draw the lobby); `next(e)` resolves to `{ clientId }`, a different return changes nothing. `$.session.surfaces()` reads the roster; a render hook still reads `e.surface` per ask.

on("session.attach", { surface: "mobile" }, ($, e, next) => next(e))

session.detach #

Payload
SessionDetachInput
Result
SessionDetachResult

Fires when a client leaves the roster: it detached, or the session ended with it attached (`e.reason`). Observe; `next(e)` echoes `{ clientId }`.

session.measure #

Payload
SessionMeasureInput
Result
SessionMeasureResult

Fires when the engine measures the session and a unit moved: after each main-thread turn, and when a rate-limit window moves a whole point.

Observe; `next(e)` echoes `{ changed }`. `$.session.usage()`'s figures, pushed, not polled: compare them with your own threshold here, call the op for the breakdown. One at a time, a burst folding into one more.

on("session.measure", ($, e, next) => (toastPast90(e.rateLimits), next(e)))

session.end #

Payload
SessionEndInput
Result
SessionEndResult

Fires once when the session ends (exit, /clear, resume, logout, signal, a `-p` run done), after its SessionEnd settings hooks; `e.reason` says which.

`e.resume.id` is `--resume`'s id; `next(e)` runs the engine's end step for the plugins (at an exit the attached clients leave): `{ sessionId }`. The SessionEnd bound afresh, 1.5 s by default; a `kill -9` raises nothing.

on("session.end", async ($, e, next) => (await keep(e.resume), next(e)))

plugin.register #

Payload
PluginRegisterInput
Result
PluginRegisterResult

Fires once per hooks module about to join the chain, at load (the set folded and built, nothing swapped in) and at reload; core allows.

Return `{ refuse: reason }` and it never joins: no hook, no noun, no tool of it; the transcript names who refused. Its judges, `$` whole, are the plugins admitted before it and the binary's; judge by `tier` and `uses`.

on("plugin.register", { tier: "user" }, () => ({ refuse: "managed only" }))

turn.start #

Payload
TurnStartInput
Result
TurnStartResult

Fires when a model turn begins, before its first model call; `next(e)` resolves to `{ turnId }`. Observe: a different return changes nothing.

turn.step #

Payload
TurnStepInput
Result
TurnStepResult

Fires when the engine is about to send a model request of a turn, main's or a subagent's (`e.agentId`); `next(e)` resolves to the whole response.

`next({ ...e, model })` or `effort` sends another; the turn, the index and the message count are pinned. An answer without `next` sends no request. It streams (StreamNext): the hook's budget counts its own code alone.

turn.complete #

Payload
TurnCompleteInput
Result
TurnCompleteResult

Fires when a model turn has ended, at the point its duration is reported; `next(e)` resolves to `{ text }`, the answer. `e.reason` says why.

Return `{ text }` with a different text to show it beneath the answer (a synopsis, a TL;DR line); the transcript's record is never rewritten. A hook that fails leaves the answer as it was.

engine.create #

Payload
EngineCreateInput
Result
EngineCreateResult

Runs while `$` is being built, once per load or reload of this plugin and before any other hook of it; `next(e)` resolves to `$` built so far.

A step may ADD nouns and WITHHOLD nouns (leave one out, or return without `next`); it may NOT REPLACE one another step added: the step fails, named with both plugins. A step that fails unloads its plugin; `$` is rebuilt.

OpEventOf # 33 events

No result map is declared for these, so a hook here returns nothing the runtime reads.

model.complete #

Payload
ModelCompleteRequest

The argument of `$.model.complete(request)`.

model.fork #

Payload
ModelForkRequest

The argument of `$.model.fork(request)`.

audio.speak #

Payload
SpeakRequest

The argument of `$.audio.speak(text, { voice })`.

session.cwd #

Payload
NoArgs

The argument of `$.session.cwd()`.

session.root #

Payload
NoArgs

The argument of `$.session.root()`.

session.model #

Payload
NoArgs

The argument of `$.session.model()`.

session.turns #

Payload
NoArgs

The argument of `$.session.turns()`.

session.id #

Payload
NoArgs

The argument of `$.session.id()`.

session.messages #

Payload
NoArgs

The argument of `$.session.messages()`.

session.repo #

Payload
NoArgs

The argument of `$.session.repo()`.

session.surface #

Payload
NoArgs

The argument of `$.session.surface()`.

session.surfaces #

Payload
NoArgs

The argument of `$.session.surfaces()`.

session.authorize #

Payload
NoArgs

The argument of `$.session.authorize()`.

session.usage #

Payload
SessionUsageArgs

The argument of `$.session.usage({ breakdown, columns })`.

prompt.read #

Payload
NoArgs

The argument of `$.prompt.read()`.

tool.list #

Payload
NoArgs

The argument of `$.tool.list()`.

tool.register #

Payload
Required<ToolSpec>

The argument of `$.tool.register(spec)`.

command.list #

Payload
NoArgs

The argument of `$.command.list()`.

command.register #

Payload
CommandSpec

The argument of `$.command.register(spec)`.

config.list #

Payload
NoArgs

The argument of `$.config.list()`.

agent.list #

Payload
NoArgs

The argument of `$.agent.list()`.

agent.register #

Payload
AgentSpec

The argument of `$.agent.register(spec)`: the agent type as the plugin defined it. A hook above rewrites any of it; the type stays the caller's.

ui.open #

Payload
PaneOpenArgs

The argument of `$.ui.open({ id, title, focus })`; a hook above the opener may retitle it or refuse it with `{ deny }`, never rename it.

ui.close #

Payload
PaneCloseInput

The argument of `$.ui.close({ id })` with `origin` `plugin`; the engine raises it too, for the person (`person`) and an unload (`unload`).

ui.panes #

Payload
NoArgs

The argument of `$.ui.panes()`.

ui.blit #

Payload
UiBlitArgs

The argument of `$.ui.blit(...)`: a Raster's `cells` or a keyed Image's `source`; a hook above may rewrite either with `next`, or `{ deny }`.

fs.ancestors #

Payload
FsAncestorsRequest

The argument of `$.fs.ancestors({ names, of, below })`.

store.keys #

Payload
NoArgs

The argument of `$.store.keys()`.

clock.now #

Payload
NoArgs

The argument of `$.clock.now()`.

clock.sleep #

Payload
ClockWait

The argument of `$.clock.sleep(ms, { signal })`; the signal does not cross, it aborts the dispatch.

clock.after #

Payload
ClockWait

The argument of `$.clock.after(ms, fn)`: the wait before `fn`, which stays in the plugin's environment and runs once the dispatch resolves.

clock.every #

Payload
ClockWait

The argument of `$.clock.every(ms, fn)`, dispatched once per period: `fn` runs each time a dispatch resolves, and the next period is asked.

settings.read #

Payload
SettingsReadArgs

The argument of `$.settings.read({ source })`.