Permissions
Everything a plugin may touch is declared in its manifest’s permissions and enforced by the plugin context object (ctx) its handlers receive. A capability the manifest doesn’t declare is still a member of ctx, but calling it throws a permission error whose message names the plugin and the capability, with not-declared as the reason — so an undeclared call fails loudly at the call site rather than doing anything. Two exceptions: ctx.toast and ctx.store are undefined when not declared, and ctx.log without log: true silently discards everything (no error, no output — the one case that fails quietly).
fs.read / fs.write
Section titled “fs.read / fs.write”"permissions": { "fs": { "read": { "static": ["${dataDir}/cache/**"], "parameter": true, "deny": ["**/.git/**"] } }}static lists glob patterns the handler may touch on its own, independent of any call argument. parameter: true means the path comes from the tool call’s own arguments — the schema property marked "format": "file-path" — and is checked at dispatch time against the calling agent’s folder; this is the shape almost every tool that reads or writes a user-supplied path should use. deny always refuses, even against a path that would otherwise match static or pass the parameter check. A handful of secret-like patterns (**/.env, **/.env.*, **/.ssh/**, **/*.pem, **/*.key) are denied whether or not you list them.
Two things about static patterns that are easy to get wrong:
- A relative pattern is resolved against the app process’s working directory, never against your plugin’s directory.
"./cache/**"does not mean the plugin’s owncache/folder. Use${dataDir}— the plugin’s data directory — as in"${dataDir}/cache/**", or an absolute path. Patterns that begin with*(such as**/.git/**) are matched as globs without any resolution. - Inside a hook handler,
parameter: truegrants nothing. A hook has no tool-call arguments and no calling-agent folder, so it runs with an empty grant set: onlystaticrules (anddeny) apply toctx.fscalls made from a hook. If a hook needs to read a file, the path has to be covered by astaticrule.
fs.list
Section titled “fs.list”"permissions": { "fs": { "list": { "static": ["${dataDir}", "${dataDir}/exports"] } } }fs.list takes only static — a list of directory paths, matched by prefix. ${dataDir} refers to the plugin’s own data directory; a relative entry such as "./data" resolves against the app process’s working directory, the same as for fs.read above, so use ${dataDir} or absolute paths. There’s no parameter mode and no deny for fs.list.
"permissions": { "fetch": { "static": ["api.example.com"], "settings": ["word-count.apiUrl"], "deny": ["internal.example.com"] }}static is a fixed allow-list of host names; settings names settings whose value is a full URL — https://api.example.com/v1, not a bare host — so a user-configured endpoint doesn’t require a manifest change. The host is taken from that URL by parsing it; a setting holding just api.example.com doesn’t parse as a URL and grants nothing. deny always refuses. Regardless of any of this, ctx.fetch refuses requests to localhost and private IP ranges outright — that guard can’t be lifted by permission.
"permissions": { "store": true }Grants ctx.store — a per-plugin key/value store.
session
Section titled “session”"permissions": { "session": { "getActive": true, "sendMessage": true } }Grants the corresponding members of ctx.session — getActive() for the active session, sendMessage(sessionId, message) to post into one.
hostTrigger
Section titled “hostTrigger”"permissions": { "hostTrigger": true }Grants ctx.triggerInbound, ctx.listChannels, ctx.getSessionHistory, and ctx.respondToHitl. It’s a separate permission from fetch on purpose: everything it reaches is the app’s own local host, not an arbitrary external endpoint, so it carries a different trust question than “can this plugin make network calls at all.”
ai.complete
Section titled “ai.complete”"permissions": { "ai": { "complete": true } }Grants ctx.ai.complete — one tool-free completion on the user’s own configured model. It’s not an agent turn: no tools run, and there’s no HITL gating of its own.
channels
Section titled “channels”"permissions": { "channels": { "getIntegrationConfig": true, "setIntegrationConfig": true } }Grants ctx.channels.getIntegrationConfig / setIntegrationConfig, used by inbound-channel plugins to read and store per-channel configuration.
settings / toast / renderHtmlToPdf / log
Section titled “settings / toast / renderHtmlToPdf / log”"permissions": { "settings": true, "toast": true, "renderHtmlToPdf": true, "log": true }Each is a plain true that exposes the matching ctx member: ctx.settings, ctx.toast, ctx.renderHtmlToPdf, ctx.log.
execution
Section titled “execution”"permissions": { "execution": { "timeout": "30s", "maxTimeout": "120s" } }Bounds a handler’s time budget. ctx.signal is always present, with a default budget when this is omitted; execution only changes the numbers.
sandbox: true
Section titled “sandbox: true”{ "sandbox": true }This one lives at the manifest’s top level, not under permissions — it runs a node handler inside the sandbox worker rather than the plugin host’s own process. It’s rejected outright for runtime: "process", since a process plugin’s executable is already its own OS process.
A plugin inherits the folder agent’s trust level for approval gating — the same level that governs everything else the agent does in that folder. Separately, a folder’s allow-list can exclude a specific plugin regardless of trust level; a tool from an excluded plugin is refused with a message naming the plugin.
- Tools — where
fs/fetchpermissions actually get exercised - Inbound channels —
hostTriggerin practice - Manifest reference — the full
permissionsfield table
