Manifest reference
What the app accepts from a plugin’s manifest, field by field. This page is derived from the loader’s validator; when the two disagree the validator wins. The authoring walkthrough — project layout, handler code, hooks, settings, inbound triggers — is under Guides.
A manifest that fails validation is refused with one message per broken rule and the plugin is not loaded.
Top level
Section titled “Top level”| Field | Required | Rule |
|---|---|---|
name |
yes | kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$). Doubles as the plugin’s identity in the installed registry and in the pluginAllowList a folder agent may carry. |
displayName |
yes | free text, shown in Connect. |
version |
yes | semver MAJOR.MINOR.PATCH with an optional -prerelease suffix. |
description, author { name, url? }, repository, icon, license, keywords[], category |
no | descriptive only. |
capabilities[] |
no | flat lowercase tags (^[a-z0-9]+([.-][a-z0-9]+)*$), e.g. channel.telegram, audio.stt. A dot-separated namespace is recommended, not enforced. Consumers resolve a capability to the first plugin tool that declares it; absent means []. |
runtime |
no | "node" (default) — handlers are JavaScript files run in the app’s plugin host; "process" — the plugin is its own executable, see below. |
command, args[] |
process only |
command is required for runtime: "process", must be relative to the plugin’s install directory (absolute on either OS is rejected — manifests are portable), and must not contain ... |
engines.filer |
no | a version range the plugin targets. |
bundled |
no | true for plugins shipped with the app. |
contributes |
no | see below. |
sandbox |
no | true runs node handlers in the sandbox worker. Rejected for runtime: "process" (the executable is already its own OS process). |
permissions |
no | see Permissions. |
runtime: "process" — not supported
Section titled “runtime: "process" — not supported”The process runtime speaks the plugin’s tools over a process transport; a tool is called by its own id, so a
tool entry must not carry handler. The following are rejected under this runtime, each with its own message:
contributes.hooks (non-empty), contributes.views (non-empty), any contributes.settings[*] of type: "action",
sandbox, and a static permissions.fs.read/write rule without "parameter": true — a process plugin does its
own OS I/O and never receives the ctx.fs proxy that enforces static rules, so such a rule would be silently
unenforced; use "parameter": true (dispatch-time enforcement) instead.
contributes
Section titled “contributes”tools[] — MCP tools the agent can call
Section titled “tools[] — MCP tools the agent can call”| Field | Required | Rule |
|---|---|---|
id |
yes | the MCP tool name. Must not contain . (dots are reserved for namespace prefixing). Legacy manifests that use name are rejected with a message asking for id. |
title, description |
yes | description is what the model reads when choosing a tool. |
schema |
yes | path to a JSON-Schema file for the tool’s arguments, relative to the plugin root; must not contain ... A property carrying "format": "file-path" (or an array whose items do) is what ties that argument to the fs.read / fs.write parameter: true checks — its value at call time is the path the dispatcher grants and checks against the calling agent’s folder; a path passed under a property without that format gets no grant, so only a static rule could admit it. A property carrying "format": "directory-path" grants that directory and everything below it — for a tool whose input is a set of files rooted in one place (a page and the scripts and data it loads); the directory is checked against the calling agent’s folder at dispatch like any grant, deny patterns still refuse inside it, and nothing beside it is granted. |
handler |
node only |
path to the handler module, relative to the plugin root; must not contain ... Required for runtime: "node", forbidden for "process". |
timeout |
no | e.g. "30s". |
fileExtensions[] |
no | extensions the tool applies to (used by the app’s context menus). |
annotations |
no | MCP tool annotations — { readOnlyHint?, destructiveHint?, idempotentHint?, openWorldHint? }, booleans only, the same names and meaning as the MCP specification; any other key is rejected. They tell the approval gate what the tool does (see Trust and allow-lists). An undeclared tool is not assumed read-only: see there for what it is treated as. |
Handler result shape. A node handler’s return value reaches the agent as one MCP text block: a string
verbatim, anything else JSON.stringify‘d. A handler that returns an MCP content envelope —
{ content: [ { type: "text", text }, { type: "image", data: <base64>, mimeType: "image/…" } ] }, every block
well-formed — is forwarded verbatim, so an image it returns reaches the model as an image on providers that accept
image tool results. A malformed envelope falls back to the text wrapping;
nothing is dropped silently. A failure is a failure to the agent: a handler that throws, or returns an object whose
success is false (the bundled plugins’ { success: false, error }), or an envelope carrying isError: true, reaches
the agent as an MCP tool error (isError). The model still reads the text; the run’s record counts the call as failed,
and a run whose every outward send failed is reported as not done. A string result is never read as a verdict. One name, one tool: the agent loop registers built-ins first, then host tools, then
plugin tools, and a later tool whose name is already taken is skipped with a warning that names its source — a
plugin tool cannot shadow a built-in or a host tool of the same name (document-essentials renamed its reader to
read_text_document for this reason).
actions[] — right-click AI actions
Section titled “actions[] — right-click AI actions”| Field | Required | Rule |
|---|---|---|
id |
yes | kebab-case. |
title, description |
yes | shown in the context menu / action dialog. |
scope |
yes | "single-file", "multi-file", "folder", or an array of those. |
kind |
no | "prompt" (default): renders prompt and opens an agent turn in chat. "tool": invokes toolId directly — no agent turn, no model. |
prompt |
prompt kind |
path to the prompt template, relative to the plugin root; must not contain ... Forbidden for kind: "tool". |
toolId |
tool kind |
must match a contributes.tools[].id in the same manifest. |
toolScope |
no, prompt kind only |
"none" (no tools this turn) or "read-only" (only tools that read the folder and its knowledge — nothing that writes, runs a command, reaches the network, or belongs to a plugin). The manifest names a scope, never a tool; the host resolves it into the turn’s tool list and enforces it on the agent loop for that one turn. A prompt that says “read the file at …” needs read-only, not none — with no tools the model cannot read and will narrate instead. Forbidden for kind: "tool" (it runs no agent turn). |
fileExtensions[], fileTypes[] |
no | when the action is offered. |
multiFileMode |
no, tool kind |
"independent" (default): one tool call per selected file. "batch": exactly one call with every selected path under the tool schema’s array-of-paths property — for a tool whose point is to combine files. |
params[] |
no | dialog inputs: { id, label, type: "select" | "text" | "per-file-text", options?, placeholder?, default?, required? }. |
generate |
no | an in-form button { label, actionId, populateParamId, forwardParamIds? } that invokes a sibling kind: "tool" action (actionId must exist in the same manifest) and fills one of this action’s per-file-text params from its result. |
hidden |
no | true keeps the action out of the context menu while it stays invocable as another action’s generate.actionId. |
settings{} — the plugin’s settings card
Section titled “settings{} — the plugin’s settings card”Keyed by setting id. Each value: { type: "string" | "boolean" | "number" | "action" | "info", title, description?, default?, secret?, required?, enum?, multiline?, min?, max?, order?, handler?, variant? }. secret: true stores the
value in Filer’s background service’s encrypted secret store (Windows DPAPI, per user — the same store the AI provider
keys use) and hands it to handlers through ctx.settings, never to the renderer in plain text. type: "action" renders a button that runs
handler (relative path); variant is "primary" | "secondary" | "danger". Not available under runtime: "process".
hooks{} — event handlers
Section titled “hooks{} — event handlers”Keyed by event name; each value is either a handler path or { handler, filter? }. Handler paths must not contain
... Event names, payloads and the filter shape are on Hooks and events (onAgentComplete, onSessionMessage, onToolCall, …). Not available under runtime: "process".
A hook for a folder’s run fires only if that folder allows your plugin. A folder’s plugin allow-list (its settings’
plugin_allow_list, which a parent folder can narrow for everything beneath it) decides which plugins’ tools a run gets,
and the same list decides which plugins’ hooks hear about the run: its completion (onAgentComplete, onSessionMessage),
an approval request (onAgentHitlRequest) and a file-change notice (onFileChangeNotify). A folder with no list reaches
every installed plugin; an empty list reaches none. So a messenger that “notifies on every completion” notifies only for
folders that allow it — if your hook stays silent for one folder, check that folder’s plugin list first. A chat opened in a
folder follows that folder’s list the same way — the plugins its AI may call and the hooks that hear its turns
(onSessionStart, onToolCall, onAgentComplete, onSessionMessage); a chat opened outside any folder with a list
reaches every plugin. Events that belong to no folder (onAppReady, onRuntimeReady, a channel registered, a file
memorized) reach every plugin.
Hooks run in Filer’s background plugin runtime, which the background service owns — with or without the app window.
Two lifecycle events differ only in what they announce: onRuntimeReady fires once per runtime start, after the
plugin’s settings are loaded — start a channel listener (polling, IMAP idle) here; onAppReady fires when the app
window is up — it is announced to the runtime by the app, so it may fire long after onRuntimeReady, or not at all on a
machine where the background service runs without the app. Every other event is raised by the background service where
the fact is made (a file-change notification, a HITL request, a channel registered, a file memorized, a chat turn).
ctx.renderHtmlToPdf renders through headless Microsoft Edge, which Windows ships, so it works in the background
runtime with or without the app window; the page is printed with no network access and no header or footer. Where Edge
is not installed it rejects with “renderHtmlToPdf is not available in this host” — treat it as optional.
commands[] — slash commands
Section titled “commands[] — slash commands”{ id, title, icon?, contexts?[], handler?, when?, fileExtensions?[] }. handler must not contain ...
views[] — plugin-contributed panels
Section titled “views[] — plugin-contributed panels”{ id, title, icon?, location?, definition? }. Not available under runtime: "process".
permissions
Section titled “permissions”Everything a plugin may touch is declared here and enforced by the plugin context it receives (ctx). An
undeclared capability is not removed from ctx — the member is still there, but calling it throws a permission
error whose message names the plugin and the capability, with not-declared as the reason (settings.*, fs.*,
triggerInbound / listChannels / getSessionHistory / respondToHitl, renderHtmlToPdf, session.*,
ai.complete, channels.*). Two exceptions: ctx.toast and ctx.store are undefined when not declared, and
ctx.log without log: true silently does nothing — no error, no output. Hook handlers receive the same
wrapped ctx with an empty grant set and no calling-agent folder, so inside a hook parameter: true grants
nothing — only static rules (and deny) apply there.
| Key | Meaning |
|---|---|
fs.read / fs.write |
{ static?: string[], parameter?: boolean, deny?: string[] }. static — glob patterns the handler may touch on its own; parameter: true — the path comes from the call’s arguments (the schema property marked "format": "file-path") and is checked at dispatch against the calling agent’s folder — a parameter write is also refused anywhere inside a folder’s .filer/ except .filer/artifacts/ (reason filer-managed; .filer/ holds the folder’s instructions and settings); deny — always refused. The ctx.fs proxy (read, write, exists, …) is where static rules are enforced. Relative static patterns are resolved against the app process’s working directory, not the plugin’s directory — "./cache/**" does not mean the plugin’s own cache/; write "${dataDir}/cache/**" (the plugin’s data directory) or an absolute path. Patterns beginning with * (e.g. **/.env) are matched as globs without resolution. |
fs.list |
{ static?: string[], parameter?: boolean, deny?: string[] } — the same three fields as fs.read, with one difference: static entries are directory paths matched by prefix (a listed directory covers everything below it), not globs. parameter: true lets ctx.fs.list take a directory named by a "format": "file-path" argument, checked at dispatch against the calling agent’s folder exactly as a read is — the shape for any tool that lists a folder the agent chooses. deny patterns are globs and always refuse. Same resolution rule for static as above: use ${dataDir} or absolute paths, not "./data". |
fetch |
{ static?: string[], settings?: string[], deny?: string[] } — allowed hosts, hosts read from named settings, and a deny list; localhost/private ranges are refused regardless (SSRF guard). static / deny entries are host names. A settings entry names a setting whose value must be a full URL (https://api.example.com/v1) — the host is taken from it with a URL parse, so a bare host name stored in the setting parses as nothing and grants nothing. |
store |
true — a per-plugin key/value store. |
session |
{ getActive?, sendMessage? }. |
hostTrigger |
true — ctx.triggerInbound, routing a resolved inbound channel message to the host’s inbound trigger. Separate from fetch on purpose: it targets only the fixed host origin. |
ai |
{ complete?: true } — ctx.ai.complete, a single tool-free completion on the user’s own configured model. Not an agent turn; no HITL gating of its own. Inline images are read by the local vision models on the local route. |
channels |
{ getIntegrationConfig?, setIntegrationConfig? }. |
settings, toast, renderHtmlToPdf, log |
true to expose the corresponding ctx member. |
execution |
{ timeout?, maxTimeout? } — handler time budget. |
Trust and allow-lists
Section titled “Trust and allow-lists”A plugin inherits the folder agent’s trust level for approval gating; a folder’s settings.json may carry a plugin
allow-list, and a tool from a plugin not on it is refused by the host’s plugin MCP endpoint (POST /plugins/mcp) with a
message that names the plugin.
How a tool is gated. The agent’s approval gate sorts every call into a category, and the folder’s trust level
decides whether that category runs, asks or is refused. A plugin tool’s category comes from its annotations:
| Annotation | Category | Asks at |
|---|---|---|
openWorldHint: true |
external — reaches outside the machine (the network, other people, other services) |
every level below Full Auto; refused in Sandbox |
destructiveHint: true |
delete |
per the trust table |
readOnlyHint: false |
modify |
per the trust table |
readOnlyHint: true, or nothing that applies |
read | never |
When several apply, the strictest wins. A tool that declares no annotations is treated by what its plugin may do: a
plugin that declares permissions.fetch, or runs as runtime: "process" (an unsandboxed native process), has its
undeclared tools treated as openWorldHint: true, and the plugin loader logs a warning naming each such tool; a
plugin that declares permissions.fs.write has them treated as readOnlyHint: false. Permissions that only touch
Filer itself (store, session, hostTrigger, channels, settings, …) imply nothing. Declare annotations on
every tool so a read-only tool of a networked plugin does not ask. This gates the agent’s calls only — a plugin’s
hooks, commands and views are not agent turns, and their outward reach is bounded by permissions.fetch and the
allow-list.
