Plugin context (ctx)
Every handler receives ctx as its second argument — (params, ctx) for a tool, (event, ctx) for a hook. A capability the manifest’s permissions 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 the reason not-declared. 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.
| Member | Signature | Requires |
|---|---|---|
ctx.plugin |
{ name, version, dataDir } |
— |
ctx.settings.get / set / getAll |
get(key): Promise<unknown>, set(key, value), getAll(): Promise<Record<string, unknown>> — getAll returns the keys under this plugin’s prefix |
settings: true |
ctx.fetch |
(url, init?): Promise<Response>, shaped like the global fetch — external hosts only; localhost and private ranges are refused. A request body is a string or bytes (FormData and streams are refused) |
fetch { static?, settings?, deny? } |
ctx.fs.read / write / list / exists |
read(path): Promise<Buffer>, write(path, data), list(dir): Promise<string[]>, exists(path): Promise<boolean> (feature-check on older hosts) |
fs.read / fs.write / fs.list |
ctx.fs.isDenied |
(path): Promise<boolean> — whether Filer’s path denylist refuses the path |
— |
ctx.exec.run |
(program, args, { cwd, env? }): Promise<{ code, stdout, stderr }> — starts a declared program; see below |
exec: [ … ] and an fs.list rule covering cwd |
ctx.browser.launch |
(): Promise<{ wsEndpoint }> — a headless browser to drive over the DevTools protocol; see below |
browser: true |
ctx.store.get / set / list / delete |
per-plugin key/value store | store: true |
ctx.toast |
({ type: 'info' | 'error' | 'success', message }) |
toast: true |
ctx.log.info / warn / error |
(msg, ...args) |
log: true |
ctx.execute |
(commandId, args?): Promise<unknown> — another plugin’s command as other-plugin.commandId |
— |
ctx.session.getActive / sendMessage |
getActive(): Promise<{ id, title, origin?, workingPath? } | null>, sendMessage(sessionId, message) |
session { getActive?, sendMessage? } |
ctx.channels.getIntegrationConfig / setIntegrationConfig |
(channelId, pluginName) → config or null; (channelId, pluginName, config) |
channels { … } |
ctx.triggerInbound |
(payload): Promise<Response> — routes an inbound channel message to Filer’s own host |
hostTrigger: true |
ctx.listChannels |
(): Promise<ChannelListEntry[]> |
hostTrigger: true |
ctx.respondToHitl |
(agentId, requestId, approved, reason): Promise<void> |
hostTrigger: true |
ctx.ai.complete |
(prompt, options?): Promise<string> — one tool-free completion on the user’s configured model |
ai { complete: true } |
ctx.renderHtmlToPdf |
(html): Promise<Buffer> — rejects with renderHtmlToPdf is not available in this host where Microsoft Edge is not installed |
renderHtmlToPdf: true |
ctx.viewData.set |
(key, data) — view data for contributes.views. ctx.viewData.get throws in a plugin’s process; keep a value you need to read back in ctx.store |
— |
ctx.channelId |
the channel the current tool call belongs to | — |
ctx.signal |
AbortSignal for the handler’s time budget — always present; execution { … } only sets the budget |
— |
ctx.fs.exists is worth checking before relying on it: a host old enough not to have it simply omits it from ctx.fs, so typeof ctx.fs.exists === 'function' is a safe feature-check for code that needs to run against older Filer installs.
ctx.signal is an AbortSignal tied to the handler’s own time budget — present on every ctx, with a default budget when the manifest declares no execution; execution.timeout / maxTimeout only change the numbers. Pass it to anything that accepts one — fetch(url, { signal: ctx.signal }), for example — so a timed-out call is actually cancelled instead of left running after the handler has already failed. The ctx calls themselves take no signal: Filer ties each one to the handler’s call, and when a tool call ends, whatever it was still waiting on is cancelled.
ctx.fs.isDenied, ctx.exec and ctx.browser arrived in Filer 0.14.0. A plugin that may also run on an older install feature-checks them — typeof ctx.fs.isDenied === 'function', typeof ctx.exec?.run === 'function', typeof ctx.browser?.launch === 'function'.
ctx.fs.isDenied
Section titled “ctx.fs.isDenied”const hidden = await ctx.fs.isDenied(path) // true for .env, keys, credentials, anything in a .ssh folder, system foldersAnswers whether Filer’s path denylist refuses path. read, write, exists and list already apply the denylist; this is for output that did not come through ctx.fs — what a program started with ctx.exec.run printed — so the handler can leave denied files out of what it returns. It needs no permission. The full list is under Permissions.
ctx.exec.run
Section titled “ctx.exec.run”const { code, stdout, stderr } = await ctx.exec.run('git', ['log', '-n', '5', '--oneline'], { cwd: folder })run(program, args, { cwd, env? }): Promise<{ code: number, stdout: string, stderr: string }>
program— a name listed in the manifest’spermissions.exec. Filer finds it onPATHand starts it without a shell.args— a list of strings, passed to the program as they are.cwd— required; the folder the program runs in. It is checked like actx.fs.listof that folder: the path denylist (a.sshfolder is refused as a whole), then the plugin’sfs.listrule.env— variables added to the environment the program inherits, for this run only.
Resolves when the program exits, with its exit code and its output decoded as UTF-8. A non-zero exit code resolves too. Rejects when:
programis not declared — a permission error with the reasonnot-declared, before anything starts;cwdis denied or not covered by thefs.listrule — a permission error;argsis not a list of strings, orcwdis missing — aTypeError;- the program is not installed — an error with the code
ENOENT; - its output is larger than 32 MB;
- the tool call ends while the program is still running — the program is killed.
ctx.browser.launch
Section titled “ctx.browser.launch”import { chromium } from 'playwright-core'
const { wsEndpoint } = await ctx.browser.launch()const browser = await chromium.connectOverCDP(wsEndpoint)launch(): Promise<{ wsEndpoint: string }>
Filer starts the system’s Microsoft Edge — headless, with a throwaway profile and no network access — and resolves the browser’s DevTools endpoint, a ws://127.0.0.1:<port>/devtools/browser/… URL. When the tool call ends, Filer kills the browser and removes its profile; a browser started from a hook or a command stays until the plugin runtime stops. Rejects when:
permissions.browseris not declared — a permission error with the reasonnot-declared;- Microsoft Edge is not installed —
ctx.browser is not available in this host; - Edge exits, cannot be started, refuses to open its DevTools endpoint, or has not opened it within 20 seconds.
- Permissions — what each
Requiresentry above means to declare - Events — the payloads a hook’s
eventargument carries
