Skip to content

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

const hidden = await ctx.fs.isDenied(path) // true for .env, keys, credentials, anything in a .ssh folder, system folders

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

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’s permissions.exec. Filer finds it on PATH and 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 a ctx.fs.list of that folder: the path denylist (a .ssh folder is refused as a whole), then the plugin’s fs.list rule.
  • 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:

  • program is not declared — a permission error with the reason not-declared, before anything starts;
  • cwd is denied or not covered by the fs.list rule — a permission error;
  • args is not a list of strings, or cwd is missing — a TypeError;
  • 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.
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.browser is not declared — a permission error with the reason not-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 Requires entry above means to declare
  • Events — the payloads a hook’s event argument carries