docs/plugins.md
CodexBar can load one local JavaScript or TypeScript file as a provider. Put a .js or .ts file in
~/.config/codexbar/providers/, or choose Settings → Plugins → Install…. Each file declares its complete authority
and settings schema in a manifest, fetches through CodexBar's sandboxed host API, and returns a generic usage snapshot.
Plugins are local files only. CodexBar has no plugin catalog, does not download plugin code or assets, and does not resolve imports. A plugin cannot use Node, browser globals, subprocesses, local files, databases, OAuth, WebViews, or arbitrary native APIs. The maximum source size is 1 MiB.
defineProvider({
id: "acme-usage",
name: "Acme Usage",
icon: { monogram: "AC", tint: "#336699" },
endpoints: ["https://api.example.com"],
auth: { type: "bearer", secret: "API_KEY" },
settings: [
{ key: "API_KEY", title: "API key", subtitle: "Create one in Acme settings.", type: "secure" },
],
async fetchUsage(ctx) {
const response = await ctx.http.getJSON("https://api.example.com/v1/usage");
return {
primary: {
usedPercent: response.json.used_percent,
resetsAt: response.json.resets_at,
windowMinutes: 300,
},
details: [{
title: "Usage",
rows: [{ label: "Requests", value: String(response.json.requests) }],
}],
};
},
});
defineProvider must be called exactly once with an object containing:
id: 1–64 lowercase ASCII letters, digits, or hyphens. It must not match a built-in provider or another installed
plugin.name: trimmed display name, 1–80 UTF-8 bytes.icon (optional): {monogram, tint}. monogram is 1–3 characters; tint is #RRGGBB. The fallback is the first
letter of name with a neutral tint. File/SVG icons are not supported.endpoints: 1–16 declared network origins. A fixed endpoint is a normalized HTTPS origin such as
https://api.example.com (no path, query, fragment, or user info). A settings-derived endpoint is
{setting: "BASE_URL", policy: "https"}, {setting: "BASE_URL", policy: "https-or-loopback-http"}, or
{setting: "BASE_URL", policy: "https-or-private-network-http"}. Its setting must be declared as plain.
https-or-loopback-http preserves the unauthenticated loopback-only rule. https-or-private-network-http also permits
authenticated HTTP for loopback, RFC 1918 IPv4, IPv4 link-local, IPv6 unique-local/link-local, and .local targets,
but only through the separate typed approval described below. Public targets always require HTTPS.auth (optional): one of the forms below. The named secret must be a declared secure setting.settings: up to 32 setting definitions. Keys contain 1–64 ASCII letters, digits, or underscores and start with a
letter. Each entry has key, title, optional subtitle, and type: "plain" | "secure" (default secure).capabilities (optional): "browser-cookies" and "http-status". With "http-status", the plugin observes
non-2xx responses itself instead of the host failing the request.cookieDomains: required with browser-cookies; a non-empty list of normalized DNS host names.fetchUsage(ctx): function returning a snapshot object or a promise for one.Authentication forms:
auth: { type: "bearer", secret: "API_KEY" }
auth: { type: "x-api-key", secret: "API_KEY" }
auth: { type: "header", header: "X-Custom-Key", secret: "API_KEY" }
auth: { type: "authorization-scheme", scheme: "Token", secret: "API_KEY" }
The host owns the authentication header; plugin request options cannot override it. Authenticated public origins must be
HTTPS; authenticated private-network HTTP requires https-or-private-network-http plus typed approval. Secure settings
can be overridden for CLI use with
CODEXBAR_PLUGIN_<PLUGIN_ID>_<SETTING_KEY>, uppercased with non-alphanumeric characters replaced by underscores. For
example, acme-usage and API_KEY use CODEXBAR_PLUGIN_ACME_USAGE_API_KEY.
ctx APIctx exists only during fetchUsage. CodexBar uses QuickJS on every platform; both QuickJS and the Apple-only
JavaScriptCore rollback engine provide ECMAScript built-ins but no browser or Node environment. Intl is
engine-dependent and unavailable in QuickJS,
so portable third-party plugins must use the host helpers below instead of ECMA-402. fetch, XMLHttpRequest, timers,
require, process, and filesystem APIs are unavailable.
await ctx.http.getJSON(url, opts?) performs GET and returns {status, headers, json}.await ctx.http.get(url, opts?) performs GET and returns {status, headers, bodyText}.await ctx.http.postJSON(url, {body, headers?}) performs JSON POST. body must be JSON-serializable.opts.headers accepts string values. Plugins cannot replace their declared auth header. opts.timeoutSeconds sets a
hard request deadline from 1 through 30 seconds; the default is 15 seconds.ctx.settings.get(key) reads a declared plain setting.ctx.settings.getSecret(key) reads a declared secure setting. Missing values return null; kind mismatches and
undeclared keys throw.ctx.fail creates classified errors for authenticationExpired, missingCredential, permissionDenied,
rateLimited, providerUnavailable, parseFailure, networkFailure, and apiFailure. Throw the returned error,
for example throw ctx.fail.rateLimited("Provider rate limit reached"); ordinary errors retain generic mapping.
Every plugin automatically gets one delayed retry when a request returns 408, 429, 500, 502, 503, or 504. A numeric
Retry-After header sets the delay; otherwise the delay is 1 second, and the host clamps it to 10 seconds. A plugin
that needs provider-specific handling—such as a non-numeric Retry-After, quota data in the error body, or a vendor
retry field—declares http-status, receives the response, and throws ctx.fail.rateLimited(message, {retryAfterSeconds}) or another transient classified failure. Both paths share one retry budget and never retry the
retry. Cancellation during the delay stops the retry.await ctx.browser.cookieHeader(domain) returns a cookie header only with the browser-cookies capability and for a
declared domain. The app imports from Chrome only. Cookie values are secret-equivalent and redacted.ctx.html.metaContent(html, name) returns the first matching quoted meta value or null.ctx.html.matchFirst(html, regexSource, flags?) returns the first capture/full match or null.ctx.log(...values) writes to the instance-scoped plugin log. Known secrets and cookie values are redacted.ctx.cache.get(key) and ctx.cache.set(key, value, ttlSeconds) provide a per-runtime memory cache. TTL is capped at
24 hours.ctx.date.now(), iso(text), unixSeconds(number), and unixMillis(number) create JavaScript dates. now() uses
the host refresh clock.ctx.date.nowMillis() returns the same host refresh clock as Unix epoch milliseconds — use it for arithmetic that
should stay deterministic under fixture clocks (the z.ai quota-rate row does).ctx.date.nextDailyReset(timeZoneIdentifier, hour) returns the next wall-clock reset in an IANA time zone.ctx.env.timeZone is the host's current IANA time-zone identifier; zero-offset GMT aliases are normalized to UTC.ctx.format.number(value, options?), usd(value), and monthDay(date) provide deterministic formatting on both
engines. Number options support minimumFractionDigits and maximumFractionDigits.ctx.jwt.decode(token) decodes (but does not authenticate) a JWT JSON payload.ctx.pct(used, limit) returns a finite percentage clamped to 0–100; non-positive limits map to 100.User-plugin requests run in an ephemeral session with no ambient cookies, credential store, or URL cache. Redirects are
rejected, the timeout is 15 seconds, Accept-Encoding: identity is sent, compressed responses always fail, and response
bytes are capped at 1 MiB. By default, the host rejects non-2xx responses and automatically retries 408, 429, 500, 502,
503, and 504 once, using a numeric Retry-After delay or 1 second when absent, clamped to 10 seconds. With http-status,
the plugin instead receives {status, headers, ...} and owns classification, including any request for the same single
delayed retry. Request URLs must match a declared, approved origin.
capabilities: ["http-status"],
async fetchUsage(ctx) {
const response = await ctx.http.getJSON("https://api.example.com/usage");
if (response.status === 429) {
const retryAfterSeconds = Number(response.headers["retry-after"] || 1);
throw ctx.fail.rateLimited("Rate limited", { retryAfterSeconds });
}
}
Declaring http-status changes the approval binding, so an installed plugin requires re-approval after adding it.
Bundled first-party providers that have cut over to JavaScript use the shared runtime's 20-second hung-script watchdog.
A timeout fails that refresh and discards the poisoned worker so the next refresh starts with a fresh context; this is
production-default and does not depend on CODEXBAR_JS_PROVIDERS.
QuickJS enforces the watchdog in-engine with JS_SetInterruptHandler, caps the runtime heap at 64 MiB, and caps the
JavaScript stack at 2 MiB. The interrupt terminates evaluation on its confined thread; timed-out scripts do not leave an
abandoned evaluation thread behind. On Apple platforms, CODEXBAR_PLUGIN_ENGINE=jsc selects the JavaScriptCore rollback
engine; the same rollback is available in Settings → Debug → Provider Plugins and takes effect after restarting
CodexBar. JavaScriptCore has no public interrupt API, so a timed-out rollback-engine context is discarded but its
abandoned evaluation thread can remain alive until process exit.
Return at least one rate window, cost object, detail section, or non-empty identity field:
return {
primary: { usedPercent: 25, resetsAt: new Date(), windowMinutes: 300 },
secondary: { usedPercent: 40, resetsAt: "2026-08-10T00:00:00Z", windowMinutes: 10080 },
tertiary: { usedPercent: 5 },
extraWindows: [{ id: "daily", title: "Daily", window: { usedPercent: 12 } }],
cost: { used: 8.5, limit: 20, currency: "USD", period: "This month", balance: 11.5 },
identity: { email: "[email protected]", organization: "Acme", loginMethod: "API key", accountID: "123" },
subscriptionRenewsAt: "2026-09-01T00:00:00Z",
dataConfidence: "exact", // exact | estimated | percentOnly | unknown
details: [{
title: "Usage summary",
rows: [{ label: "Requests", value: "1,240", secondaryValue: "Last 30 days" }],
chart: {
kind: "bars", // bars | line
title: "Daily spend",
unit: "USD",
points: [{ label: "2026-08-01", value: 4.25 }],
},
}],
};
Percentages must be finite and are clamped to 0–100. Window minutes are positive integers. Cost requires finite used
and a three-letter uppercase currency. Dates are JavaScript Date values or ISO-8601 strings. Snapshot identity is
always scoped to the manifest's instance ID. Data confidence defaults to unknown. Details allow at most 8 sections, 24 rows per section, 120 chart points,
and 120 characters per detail string. Wrong types and limit violations fail the whole fetch instead of truncating it.
An identity-only snapshot is useful for balance-only or zero-usage provider states and renders its available account,
organization, plan/login-method, and account-ID fields in the menu and CLI. An empty object, an empty identity object,
or metadata such as confidence and subscription dates without displayable usage or identity remains invalid.
codexbar-plugin.d.ts is the canonical authoring
contract for defineProvider, the ctx host API, manifests, and usage snapshots. Bundled plugins may use that contract
directly as .ts sources. Scripts/regenerate-plugin-js.sh transpiles them with the vendored Sucrase build into
committed sibling .js files; the runtime continues to load only those JavaScript files, so bundled TypeScript has no
runtime compilation cost. make check verifies both the TypeScript contract and generated-file freshness.
For bundled-plugin work, run make format after editing TypeScript so the committed JavaScript is regenerated. Do not
edit a generated sibling .js file directly.
TypeScript files are transpiled by the selected plugin engine with the bundled Sucrase 3.35.1 build using its
typescript transform. Use ordinary
type syntax but no module imports, JSX, decorators, or runtime TypeScript features that require module resolution.
Transpiled output is cached in ~/Library/Caches/CodexBar/plugins/ under a filename containing the SHA-256 of the source
and the Sucrase version. An unchanged file is a cache hit; any source or compiler-version change produces a new key.
Transpile failures appear as that plugin's Settings error.
.js/.ts file into the providers directory..local origins, type every normalized origin exactly before approval.Approval records live outside plugin files under ~/Library/Application Support/CodexBar/plugin-approvals.json. A
change to instance ID, normalized origins, auth mode/header, secure setting names, capabilities, or cookie domains
invalidates approval before the next request. There is no bulk approval or import path.
Bundled first-party plugins do not use the interactive plugin-approval flow. The private-network HTTP policy is therefore accepted for bundled code only for LLM Proxy and LiteLLM, whose existing Swift providers already permit exactly those targets. Other bundled providers fail manifest validation if they request that policy.
codexbar plugins list shows locally discovered plugins. codexbar plugins fetch <id> displays the same approval
fields and can approve only from an interactive terminal; redirected/headless input fails closed. Browser-cookie plugins
are app-only and fail closed in the CLI.
Delete from Settings with Delete…. CodexBar removes the plugin file, matching TypeScript cache output, approval, per-instance settings and secrets, and per-instance usage history. Invalid plugin files are listed with their validation error and can also be deleted.
Treat a plugin like code you run locally, even though its host capabilities are narrow. Read the manifest and source, verify every origin, and avoid installing files from untrusted repositories. Approval grants the listed origin network authority; DNS changes after approval are outside CodexBar's threat model. Secrets are never placed in URLs or logged, redirects cannot forward authentication, and undeclared settings/cookies/origins fail closed.
Plugins support the macOS app plus the macOS and Linux CLIs. They are excluded from widgets and all built-in-provider-only surfaces (status feeds, token accounts, OAuth, browser automation, storage probes, local cost scanners, and provider specific payloads). Rendering is limited to generic snapshots and declarative details. There are no remote catalogs, downloaded plugins/assets, custom SVGs, imports, arbitrary local I/O, or compatibility fallback from an unknown ID to a built-in provider.