Browse has a plugin system that lets you add custom commands and hook into the command lifecycle. Plugins are TypeScript or JavaScript files that export a BrowsePlugin object.
- Create a plugin file:
// plugins/hello.ts
// Import the plugin types from the browse repo's src/plugin.ts
// (adjust the relative path to where browse is checked out)
import type { BrowsePlugin } from "../path/to/browse/src/plugin.ts";
const plugin: BrowsePlugin = {
name: "hello",
version: "1.0.0",
commands: [
{
name: "hello",
summary: "Say hello",
usage: "browse hello [name]",
handler: async (ctx) => {
const name = ctx.args[0] ?? "world";
return { ok: true, data: `Hello, ${name}!` };
},
},
],
reporters: [
{
name: "teamcity",
render: ({ flowName, results }) =>
`##teamcity[testSuiteFinished name='${flowName}' count='${results.length}']`,
},
],
};
export default plugin;- Register it in
browse.config.json:
{
"environments": {},
"plugins": ["./plugins/hello.ts"]
}- Use it:
browse hello Dan
# Hello, Dan!A plugin default-exports a BrowsePlugin object:
type BrowsePlugin = {
name: string; // Unique plugin name
version: string; // Semver version
commands?: PluginCommand[];
reporters?: CustomReporter[];
hooks?: PluginHooks;
};Each command defines a name, help text, and an async handler:
type PluginCommand = {
name: string; // Must not collide with built-in or other plugin commands
summary: string; // One-line description for `browse help`
usage: string; // Full usage text for `browse help <command>`
flags?: string[]; // Known flags for validation (e.g. ["--json", "--verbose"])
timeoutExempt?: boolean; // If true, exempt from global --timeout
handler: (ctx: CommandContext) => Promise<Response>;
};Every handler receives a context bag with everything it needs:
type CommandContext = {
page: Page; // Active Playwright page
context: BrowserContext; // Session's browser context
config: BrowseConfig | null; // Loaded browse config
args: string[]; // Command arguments
sessionState: Record<string, unknown>; // Per-plugin, per-session state
request: {
session?: string;
json?: boolean;
timeout?: number;
};
};sessionState persists across commands within a session and is scoped per plugin. Use it to track state between commands without managing your own maps.
Handlers return the standard browse response:
type Response =
| { ok: true; data: string }
| { ok: false; error: string };Plugins can contribute custom flow reporters that are available through browse flow --reporter <name> and browse test-matrix --reporter <name>.
type CustomReporter = {
name: string; // Must not collide with built-in reporters like junit or json
render: (ctx: ReporterRenderContext) => string;
};
type ReporterRenderContext = {
flowName: string;
results: StepResult[];
durationMs: number;
};Reporter names must be unique across all loaded plugins. Collisions with built-in reporters or another plugin reporter are skipped with a warning.
Plugins can hook into the lifecycle of any command (built-in or plugin):
type PluginHooks = {
init?: (config: BrowseConfig | null) => Promise<void>;
beforeCommand?: (cmd: string, ctx: CommandContext) => Promise<Response | void>;
afterCommand?: (cmd: string, ctx: CommandContext, response: Response) => Promise<void>;
cleanup?: () => Promise<void>;
};| Hook | When | Use case |
|---|---|---|
init |
Daemon startup, after plugin is loaded | Set up resources, validate config |
beforeCommand |
Before any command executes | Auth gating, logging, rate limiting. Return a Response to short-circuit |
afterCommand |
After any command executes | Logging, metrics, telemetry |
cleanup |
Daemon shutdown | Release resources, flush buffers |
Plugins are discovered from two sources:
List plugin paths in browse.config.json:
{
"plugins": [
"./plugins/my-plugin.ts",
"/absolute/path/plugin.ts",
"browse-plugin-lighthouse"
]
}- Relative paths resolve from the config file's directory
- Absolute paths are used as-is
- Bare names are resolved as npm packages via
import()
Any .ts or .js files in ~/.browse/plugins/ are automatically loaded. This is useful for personal plugins that apply across all projects.
Config-declared plugins take precedence on name collision.
Browse can also help you discover official starters and published community plugins before you install them:
browse plugins official
browse plugins search slack
browse plugins search jira --limit 10browse plugins officiallists first-party plugin starters, including their in-repo source pathsbrowse plugins search <query>searches npm for packages tagged with thebrowse-pluginkeyword- Add the global
--jsonflag for machine-readable output
Browse ships first-party starter plugins under examples/plugins/ for common team integrations:
./examples/plugins/slack/index.ts— send a message to a Slack webhook./examples/plugins/discord/index.ts— send a message to a Discord webhook./examples/plugins/jira/index.ts— create a JIRA issue for the current page
Register them directly in browse.config.json while the standalone packages are being formalised:
{
"plugins": ["./examples/plugins/slack/index.ts"]
}Each plugin directory includes its own README with the required environment variables and usage examples.
The plugin system is designed to be resilient:
- Load failures are non-fatal — a bad plugin is skipped with a warning, other plugins and the daemon still work
- Command handlers are wrapped in try/catch — a throwing handler returns
{ ok: false, error: "Plugin 'name' error: ..." }instead of crashing the daemon - Hook errors are isolated — a throwing
beforeCommandhook produces an error response; a throwingafterCommandorcleanuphook is silently ignored - Init failures don't prevent registration — if
initthrows, the plugin's commands and hooks are still registered - Name collisions are rejected — a plugin command that collides with a built-in command or another plugin is skipped with a warning
If your command defines flags, browse validates them before your handler runs. Unknown flags produce a helpful error:
{
name: "audit",
flags: ["--json", "--verbose", "--threshold"],
handler: async (ctx) => { /* ... */ },
}browse audit --unknown-flag
# Error: Unknown flag '--unknown-flag' for command 'audit'.Commands without flags skip validation (useful for commands that accept freeform arguments).
Plugin commands go through the same timeout system as built-in commands (default 30s, configurable via --timeout). Set timeoutExempt: true for long-running commands like crawlers or report generators.
For npm distribution, create a package with a default export:
browse-plugin-foo/
index.ts # exports default BrowsePlugin
package.json
Users install and reference it by package name:
npm install browse-plugin-foo{
"plugins": ["browse-plugin-foo"]
}For type safety during development, import the types from the browse repo's src/plugin.ts (browse is not published to npm, so use a relative path to a checkout — see examples/plugins/ for working examples):
import type { BrowsePlugin, CommandContext } from "../path/to/browse/src/plugin.ts";