Skip to content

Hooks and events

A hook is a handler your plugin runs when something happens in the app — a session starting, a file being indexed, an agent turn finishing. Unlike a tool, nothing calls a hook on purpose; the app fires it as a side effect of something else happening.

"hooks": {
"onAgentComplete": "./hooks/on-complete.js",
"onFileMemorized": {
"handler": "./hooks/on-memorized.js",
"filter": { "extensions": [".md"] }
}
}

A hook value is either a plain handler path, or an object with handler plus a filter that narrows which occurrences of the event actually invoke it — onFileMemorized above only fires for Markdown files. The only filter is extensions, and it applies to events whose payload carries a path (onFileMemorized); on an event without path the filter matches everything.

Every handler has the same signature: (event, ctx). The event object is passed through unmodified — whatever shape is documented below is exactly what your handler receives.

Hooks are not available under runtime: "process" — see Process runtime.

  • onAppReady {} — the app has finished starting up.
  • onSessionStart { sessionId, channelId } — a chat session began.
  • onAgentComplete { sessionId, channelId, result: string | null, duration } — an agent turn finished.
  • onToolCall { toolId, params, result, success } — a tool call completed during a turn.
  • onFileMemorized { path } — a file was indexed into the folder’s knowledge base; supports the extensions filter shown above.
  • onPluginSettingsChanged { key, oldValue, newValue } — a plugin setting was saved from the app’s settings UI. Delivered to every plugin’s handler, whichever plugin’s setting changed, so check that key starts with your own prefix. oldValue is always undefined; newValue is the saved value. A handler’s own ctx.settings.set does not fire it.
  • onFileChangeNotify { agentId, channelId, folderPath, folderDisplayName, changes, summary, isDigest } — a summarized notification of file activity, potentially batched into a digest.
  • onSessionMessage { sessionId, channelId, result? } — a message was produced in a session.
  • onChannelChanged { channelId, path, action: 'registered' | 'unregistered' } — an inbound channel was bound to or released from a folder.
  • onAgentHitlRequest { agentId, executionId, requestId, action, target, description, channelId } — the agent is asking for human-in-the-loop approval before proceeding.
  • onSessionEnd, onFileChange and onActionCreated are declared in the manifest schema but are never emitted. A handler registered for any of these validates cleanly and simply never runs. For file activity, use onFileMemorized (per indexed file) or onFileChangeNotify (the agent’s summarized digest).

The result trap in onAgentComplete / onSessionMessage

Section titled “The result trap in onAgentComplete / onSessionMessage”

result on both onAgentComplete and onSessionMessage is a plain string or null — never an object. If you write event.result?.summary expecting a structured result, you get undefined silently; there is no summary field, or any other field, to read off of it.

A chat session driven from the UI’s chat panel streams its result to the renderer as it’s produced, so result is null on onAgentComplete and absent on onSessionMessage for that kind of session. If you need the actual content of a UI-driven session, call ctx.getSessionHistory(sessionId) instead of reading event.result. That call requires the hostTrigger permission.