Permissions
Everything a plugin may touch is declared in its manifest’s permissions and enforced by the plugin context object (ctx) its handlers receive. The block is required: Filer 0.12.0 and later neither install nor load a manifest without it, so write "permissions": {} for a plugin that touches nothing. 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).
A plugin’s own process
Section titled “A plugin’s own process”A node plugin’s code — its tools, hooks, commands and setting actions — runs in a process of its own, started under Node’s permission model. On its own, that process may:
- read the plugin’s install folder (and the folder it really resolves to, when it is installed through a link), the runtime’s own bundle, and the plugin’s temp folder;
- write only the plugin’s temp folder.
TMP,TEMPandTMPDIRpoint there, and it is emptied each time the process starts — keep anything that must last inctx.storeor under${dataDir}throughctx.fs.
It may not start programs, worker threads or native addons. Everything else — files, programs, a browser, Filer’s host, settings — reaches the plugin through ctx, which is where the rules on this page are enforced. Code that reaches past ctx is refused by Node (ERR_ACCESS_DENIED), and the call fails with a message saying the plugin tried to reach a file or start a program by itself.
Node’s permission model keeps a plugin’s code to what Filer grants it; it is not a sandbox against code written to escape it. Make network requests with ctx.fetch — that is where the fetch rules below apply.
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.
Filer’s path denylist applies to every plugin whether or not you list anything: secret-like files — .env and .env.*, credentials*, aws-credentials, *.pem, *.key, *.pfx, and SSH keys of every type (id_rsa*, id_ed25519*, id_ecdsa*, id_dsa*) — anything inside a .ssh folder, and the Windows system folders (C:\Windows, C:\Program Files, C:\Program Files (x86), C:\ProgramData) are refused to read, write and exists, with the same message Filer’s own file tools give. ctx.fs.isDenied(path) answers whether the denylist refuses a path; it needs no permission, and it is how a plugin leaves denied files out of output that did not come through ctx.fs (see exec).
A write anywhere inside a folder’s .filer/ other than .filer/artifacts/ is refused with a permission error whose rule begins filer-managed: — .filer/ holds the folder’s instructions and settings. This holds for static rules too; the plugin’s own data directory is not affected.
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 the same three fields as fs.read. static is a list of directory paths matched by prefix — a listed directory covers everything below it. ${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. parameter: true lets ctx.fs.list take a directory named by a call argument marked "format": "file-path", checked at dispatch time against the calling agent’s folder exactly as a read is — the shape for any tool that lists a folder the agent chooses:
"permissions": { "fs": { "read": { "parameter": true }, "list": { "parameter": true } } }deny takes glob patterns and always refuses. The path denylist described under fs.read applies here too: a denied folder (a system folder) cannot be listed, and denied entries are left out of a list result — listing a .ssh folder returns no entries — so a tool that lists a folder never hands a secret file’s name to the model.
"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 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.
browser
Section titled “browser”"permissions": { "browser": true }Grants ctx.browser.launch(), the only way a node plugin drives a browser. Filer starts the system’s Microsoft Edge for the call — headless, with a throwaway profile and no network access (host names resolve to nothing, and requests to IP addresses, localhost included, are refused) — and resolves { wsEndpoint }, a DevTools endpoint on loopback for Playwright’s chromium.connectOverCDP. When the tool call ends, Filer kills the browser and removes its profile; a browser started from a hook or a command, which has no call to end, stays until the plugin runtime stops.
What it refuses:
- without
browser: true, the call throws a permission error with the reasonnot-declared; - where Edge is not installed, it rejects with
ctx.browser is not available in this host— treat the browser as optional; - a browser that has not opened its endpoint within 20 seconds is killed and the call rejects.
"permissions": { "fs": { "list": { "parameter": true } }, "exec": ["git"]}exec is a list of program names — letters, digits, ., _ and -; no path, no spaces. It grants ctx.exec.run(program, args, { cwd, env? }), the only way a node plugin starts a program. Filer finds the program on PATH by its name and starts it without a shell, so args reach it as they are — no quoting, globbing or piping. It resolves { code, stdout, stderr }; a non-zero exit code resolves too, and reading it is up to you.
cwd is required and is checked exactly like a ctx.fs.list of that folder: the path denylist, then your fs.list rule — so a plugin that declares exec also declares an fs.list rule that covers the folders it runs programs in. With "parameter": true, as above, that is a folder named by the tool call’s arguments, inside the calling agent’s folder. A folder whose contents the denylist refuses — a .ssh folder — is refused as a whole.
What it refuses:
- a program not named in
execthrows a permission error with the reasonnot-declared, before anything starts; argsthat are not a list of strings, or a missingcwd, throw aTypeError;- a program that is not installed rejects with
ENOENT; - output larger than 32 MB rejects rather than being cut short;
- when the tool call ends — its time budget runs out, or it is cancelled — a program still running is killed and the call rejects.
exec is not available to runtime: "process" plugins — such a manifest is refused, since that executable starts its own programs.
A declared program runs with the arguments you pass, as the person’s account, and the file rules on this page do not bind what it then reads. If your plugin returns what a program produced — a file list, a diff, the contents of a commit — ask ctx.fs.isDenied about each path in it and leave the denied ones out, so a committed .env or key file never reaches the model through your tool.
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.
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
