Skip to content

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.

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.

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.

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).

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".

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.

{ id, title, icon?, contexts?[], handler?, when?, fileExtensions?[] }. handler must not contain ...

{ id, title, icon?, location?, definition? }. Not available under runtime: "process".

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.

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.