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.
Declaring a hook
Section titled “Declaring a hook”"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.
Events
Section titled “Events”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 theextensionsfilter 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 thatkeystarts with your own prefix.oldValueis alwaysundefined;newValueis the saved value. A handler’s ownctx.settings.setdoes 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,onFileChangeandonActionCreatedare 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, useonFileMemorized(per indexed file) oronFileChangeNotify(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.
- Settings — reading the value a change delivered
- Inbound channels —
onSessionMessageandonAgentHitlRequestused together to relay a conversation - Manifest reference — the
hooks{}field shape
