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”onRuntimeReady {}— the plugin runtime has loaded your plugin and its settings are readable; start polling loops and listeners here. It fires in the background service, with or without the app window.onAppReady {}— the app window has finished starting up. Only for a hook that needs the window.onSessionStart { sessionId, channelId }— a chat session began.onAgentComplete { sessionId, channelId, result: string | null, duration, outcome, reason }— a chat turn or a folder’s run finished.outcomeiscompleted,unfulfilled(it ended without doing everything asked) orfailed;reasonis Filer’s sentence for why when it is notcompleted.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 alwaysnull;newValueis the saved value, ornullfor a secret setting (read it withctx.settings.get). A handler’s ownctx.settings.setdoes not fire it. A listener you started inonRuntimeReadyfrom settings — a polling switch, a token, an account — restarts here when one of those keys changes; otherwise the change waits for the next start of Filer.onFileChangeNotify { agentId, channelId, folderPath, folderDisplayName, changes, summary, isDigest }— a summarized notification of file activity, potentially batched into a digest.onSessionMessage { sessionId, channelId, result }— the reply to a conversation that came in on your plugin’s channel, raised to your plugin alone, with the reply’s text inresult.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.
What it holds depends on where the turn came from:
- A chat in the app’s own panel raises
onAgentCompletewith the reply’s text — its first 500 characters, followed by…when it is longer — and neveronSessionMessage: a conversation at the person’s desk is not copied to a messenger unasked. - A folder’s run (a file change, a schedule, a manual start, a hand-off) raises
onAgentCompletewith what the run did, ornull, and noonSessionMessage. - A conversation that came in on your plugin’s channel raises
onSessionMessageto your plugin alone, always with the reply’s text inresult— that is the hook a messaging plugin answers from.
A plugin that offers “send me every result” does it from onAgentComplete, and says what outcome says — never “done” for a turn that failed. There is no call that reads a whole conversation’s transcript.
- Settings — reading the value a change delivered
- Inbound channels —
onSessionMessageandonAgentHitlRequestused together to relay a conversation - Manifest reference — the
hooks{}field shape
