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

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. 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 the plugin secret store (plugins/secrets.enc, encrypted with the OS keychain through Electron’s safeStorage when available) 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".

{ 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; 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[] } — directory paths matched by prefix; same resolution rule as above, so 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 truectx.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 at the Plugin MCP Server with a message that names the plugin.