Skip to content

Tool reference: CLI and configuration

The browxai bin dispatches sub-commands; with no args it starts the MCP server (default).

  • browxai doctor [--verbose]: environment + connectivity health-check. The default output is one ✓/✗ line per area (node, install, workspace, browser, capabilities, attach, settings, plugins), failures first with the next step; --verbose prints every row below. Checks (Node 22 or later? build present? workspace writable? BROWX_TEST_ATTRIBUTES set? BROWX_ATTACH_CDP reachable? a browser to launch: the installed Google Chrome, or the bundled Chromium when Chrome is absent?), plus a plugins section: plugins.json parseable, every declared plugin installed (for drift, run browxai plugin sync), no orphan installs in plugins/node_modules/, lock health (plugins-lock.json present, contentSha256 pins match the installed contents, no stale pins), and per-plugin manifest sanity without executing any plugin code (apiVersion vs the runtime contract, namespace validity + uniqueness, declared capabilities ⊆ the enabled set, dependsOn resolvable + acyclic). − rows (e.g. no plugins declared) are informational, and an informational row that is not ready (chrome, chromium, firefox, webkit, android, safari, electron) prints an enable with: line; any ✗ fails doctor. Exits 0 if all checks pass, 1 if any fails, 2 for an unknown flag.
  • browxai chrome start [--port N] [--profile NAME] [--force] [--insecure] [--disable-background-throttling]: launch an attachable installed Google Chrome (not the bundled Chromium; it refuses when Chrome is not installed) on the workspace’s default profile (<workspace>/profile, or BROWX_DEFAULT_PROFILE), the same profile persistent sessions use, or <workspace>/profiles/NAME with --profile. It refuses a profile another Chrome already holds (Chrome’s SingletonLock), and --force launches anyway. PID stored at $BROWX_WORKSPACE/chrome.pid. --insecure opts into --disable-web-security (use only against test/dev targets). --disable-background-throttling launches with --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding, so agents driving a backgrounded tab on this attach target keep full timer / requestAnimationFrame rate, the same knob as open_session({ backgroundThrottling:"disabled" }), applied at the launcher because an attached Chrome’s flags are fixed at launch.
  • browxai chrome login [--url URL] [--profile NAME] [--force]: open the installed Chrome on the same profile with no remote debugging port and no automation flags, so a person can clear a bot-protection challenge and sign in with nothing attached to the browser. The cookies stay in the profile. Quit that Chrome when done (it is released only on quit), then browxai sessions on the profile reuse them. Same held-profile refusal and --force as start. See Sites behind bot protection.
  • browxai chrome stop / browxai chrome status: clean teardown / liveness check. start records the profile directory it launched on in $BROWX_WORKSPACE/chrome.profile, next to chrome.pid. stop confirms the process is that Chrome before stopping it: it reads the PID’s command line (/proc/<pid>/cmdline on Linux, ps on macOS, tasklist on Windows) and sends no signal, exiting 1, when the command line cannot be read or is not a Chrome launched with --user-data-dir=<the recorded profile directory> (the workspace default profile when only an older chrome.pid exists; Windows reports the image name only, so the directory is not compared there). It reads the command line again before the SIGKILL that follows an ignored SIGTERM, and skips the SIGKILL when the PID no longer belongs to that Chrome.
  • browxai init <workspace> [--test-attrs a,b,...] [--no-write] [--no-write-user-config] [--no-smoke]: one-command start. Creates <workspace>/.browxai/, writes a workspace-scope .mcp.json with both managed + attached MCP entries, sniffs the consumer codebase for the dominant test-attribute convention and orders BROWX_TEST_ATTRIBUTES accordingly. Detects Claude Code (on PATH), Cursor, Codex and Claude Desktop and prints the exact command or snippet for each; by default it also adds browxai to the user-level config of each of Cursor, Codex and Claude Desktop that it finds (Claude Code has no file it edits, so it only gets its printed command). It prints the diff, then writes it without asking. A config that exists is merged, never replaced: other entries stay, the file mode is kept, a symlinked config is followed to the file it points at, and the old file is left as <config>.browxai-bak. A missing file is created. A file that already has a browxai entry is left as it is, and a file browxai cannot edit safely (JSON with a comment, a Codex file that already mentions browxai) is left untouched with the by-hand steps printed. Those cases exit 0. A read or write error on a config (permission denied, a failed rename) prints the by-hand steps for that client, still processes the remaining clients, and then exits 1. --no-write-user-config skips the user-level write and keeps the rest, and --no-write prints each diff and writes nothing at all. --write-user-config [<client>] is the old opt-in. It is accepted and does nothing extra, since the write is the default. With a client (cursor, codex or claude-desktop) it limits the write to that one, and then a client browxai cannot edit (claude-code) exits 1, as it always did. Combining it, bare or with a client, with --no-write-user-config exits 2. Ends with a smoke test: it starts the server over stdio with a clean environment and an empty temp workspace, opens a headless incognito session on about:blank, takes one snapshot, closes, and prints ok: first snapshot in N ms or a plain error (exit 1). It never opens an attached or real-profile session. --no-smoke skips it, and --no-write skips it too.
  • browxai serve --socket <path>: run the server on a Unix socket (or a Windows pipe name) instead of stdio. The socket is created owner-only (0600). A relative path is resolved to an absolute one, and a leftover file at the path is removed only when it is a socket. Anything else there, or a missing --socket, exits 2. A connection whose message line passes 32 MiB is closed.
  • browxai plugin <sub>: manage browxai plugins. Subcommands: install <pkg> / remove <pkg> / list / info <pkg> / upgrade [<pkg>] / sync. All ops write under the workspace root (the declarative plugins.json, the install dir at plugins/, and the auto-generated plugins-lock.json pin). Every command emits a “Server restart required” notice: plugin lifecycle is resolved-once-at-server-start. See Plugins overview and Authoring plugins.

browxai ships a v1 plugin runtime that lets external packages register namespaced tools on the MCP + SDK surface. The runtime is in-process JS modules only (v1), the lifecycle is resolved-once-at-server-start, and tool registration is globally namespaced (<namespace>.<tool>; plugins cannot override or wrap core tools).

  • Install model:

    • Kalebtec-maintained plugins ship in the monorepo at packages/plugins/<name>/ and publish as @browxai/plugin-<name>.
    • Community plugins are browxai-plugin-<name> or @<org>/browxai-plugin-<name> on npm, installed via browxai plugin install <pkg>.
    • Local/dev plugins install via file path (browxai plugin install file:./my-plugin/), trust-tagged local.
  • Three files under the workspace root carry the reproducibility surface:

    • plugins.json: declarative truth of which plugins should load.
    • plugins/node_modules/: pnpm-managed install dir.
    • plugins-lock.json: auto-generated {version, sha256, source} pin per plugin.
  • Lifecycle: set_config({plugins}) persists into config.json but takes effect on next restart (mirrors capabilities). The pluginsPendingRestart flag on get_config({scope:"resolved"}) mirrors capabilitiesPendingRestart and surfaces the divergence between the live and persisted values.

  • Inter-plugin composition: plugin manifests declare dependsOn: [{plugin, version}]. At server start the runtime topo-sorts the graph and rejects cycles loudly before any plugin runs. At runtime api.callTool(name, args) enforces the call graph: a call to a tool owned by a plugin NOT in this plugin’s transitively-declared dependsOn set is rejected with {ok:false, code:"plugin-call-graph-violation"}. Plugins cannot override or wrap core tools; namespace prefix is mandatory.

  • MCP tools:

    • plugins_list() returns array of {name, namespace, version, trust, capabilities, dependsOn, status, declaredAt, enabledAt?}. status ∈ loaded | disabled-by-capability-mismatch | disabled-by-cycle | disabled-by-dep-missing | disabled-by-namespace-conflict | load-error. Capability read.
    • plugins_info({name}) returns full manifest dump + transitive dep set + tools registered + their schemas. Capability read.

See Authoring plugins for the full author guide (manifest fields, capability rules, dep declarations, call-graph enforcement, trust tiers, local-dev workflow, npm publishing, the typed SDK seam), Plugins overview for the marketplace index + install/sync flows, and First-party plugins for the per-tool reference on the shipped @browxai/plugin-* set.

browxai is configured through the MCP-managed config store. No env vars and no hand-edited files are required. Precedence, lowest to highest:

built-in defaults < env (legacy BROWX_*) < user < project < session (open_session)
  • get_config({ scope? }): resolved merged view by default; pass scope ∈ {defaults,env,user,project,session,resolved} for one raw layer.
  • set_config({ scope: "user"|"project", patch }): the only supported way to persist config. Writes <workspace>/config.json (machine-managed; do not hand-edit). Arrays replace; unstable.* shallow-merges. Takes effect for sessions opened after the call.
  • reset_config({ scope: "user"|"project" }): clears that persistent layer.

Policy keys can only tighten. The server’s environment (or the built-in default where it leaves a key unset) is the ceiling for every policy key, and a saved or session layer can only tighten it: capabilities drop only (ceiling BROWX_CAPABILITIES), confirmRequired add only (BROWX_CONFIRM_REQUIRED), allowedOrigins narrow only, exact strings, where an empty or disjoint list falls back to the env list (BROWX_ALLOWED_ORIGINS), blockedOrigins add only (BROWX_BLOCKED_ORIGINS), disableWebSecurity off only (BROWX_DISABLE_WEB_SECURITY), plugins a subset (BROWX_PLUGINS). set_config refuses a loosening patch with { ok:false, error:"capabilities-not-widenable", widening:[…] } or { ok:false, error:"policy-not-loosenable", loosening:{ <key>: […] } }, and at every server start the saved layers are clamped the same way, with a warning naming what was ignored. Loosening a key is an edit to the server’s environment and a restart. A config.json browxai cannot parse fails the start.

BROWX_CONFIG_READONLY=1 leaves set_config, reset_config and approve_actions unregistered: they are absent from tools/list, batch cannot reach them, and the config store refuses writes as a second layer. get_config and list_approvals stay. For embedders that manage config outside the agent session.

Config keys: testAttributes, capabilities, confirmRequired, allowedOrigins, blockedOrigins, headless, channel, defaultDevice, defaultViewport, actionTimeoutMs, disableWebSecurity, hideOverlaySelectors, plugins, and a free-form unstable namespace for experimental / feature-flag knobs (not stable across versions).

channel (string, default unset): the Playwright browser channel chromium sessions launch: "chrome", "msedge", "chrome-beta", "chrome-dev", "msedge-beta", or "chromium" for Playwright’s bundled build. Unset launches the operator’s installed Google Chrome and falls back to the bundled Chromium, with a logged warning naming why, when Chrome is not installed. A named channel is taken as given and fails when its browser is missing (browxai does not download it). A launch with extensions and no channel stays on the bundled build, because branded Chrome no longer honours --load-extension. The browser version tracks the operator’s install and may differ from the version browxai pins, so a Chrome session runs a browser browxai’s own keystone lane (bundled Chromium) never ran. Layering is the usual one: open_session({ channel }) over config channel over the BROWX_CHANNEL env. Chromium only: Firefox resolves its own channel from BROWX_FIREFOX_CHANNEL, and WebKit / Safari / Android ignore it.

actionTimeoutMs (anti-wedge): hard deadline (ms) applied to every action body, eval_js, and the read CDP paths (snapshot/find/text_search/inspect). Default 5000. Every action/read tool also takes a per-call timeoutMs override. The deadline is a Promise.race at the dispatch boundary: a wedged page.evaluate/CDP call returns a structured ok:false “anti-wedge timeout” within the deadline instead of stalling forever (the orphaned op can’t be cancelled but the agent is unblocked). Clamped to [1, 3600000] (1 h hard ceiling); an over-ceiling request is clamped and a deterrent warning is added to the result. An action needing >5 s is almost always a no-op or a wedged page op. Raise timeoutMs only for one specific known-slow call, never as a blanket. wait_for’s timeoutMs is both its max wait and its deadline (a wait is meant to wait). await_human is human-paced (5 min default, 1 h hard cap, no infinite wait; the only previously-unbounded path is closed). watch/sample/batch are bounded by their own durationMs / per-inner-call deadlines.

disableWebSecurity (dangerous opt-in): off by default. When on, managed + incognito sessions launch with --disable-web-security --disable-site-isolation-trials: SOP/CORS off browser-wide (any origin can reach any server). For CORS-less-API / cross-origin QA. attached/BYOB is unaffected (externally launched; its flags are whoever started it’s responsibility). Loud warning at server boot and per session launch. Only the operator turns it on, with BROWX_DISABLE_WEB_SECURITY=1 in the server’s environment; set_config({ patch:{ disableWebSecurity:false } }) can turn it off for sessions opened afterwards, and a true from config is refused. Up to v0.10.1 it was set through set_config, which the agent can call. Same posture class as eval / network-body; see Threat model.

hideOverlaySelectors (string[], default [], off): CSS selectors for chrome/overlay elements (dev-build HMR widgets, devtools iframes, cookie/consent banners) that intercept coordinate clicks or pollute the snapshot. The server injects a CSS-only init script that applies pointer-events:none; display:none to matches on every navigation: non-destructive (no node removal, the DOM is intact for assertions) and no agent JS (the selectors come from operator-managed config, never the page). Resolved fresh per open_session (no restart needed after set_config). Prefer this over hand-rolled per-session eval_js removal. Also mappable from the legacy BROWX_HIDE_OVERLAY_SELECTORS env (comma-separated).

The BROWX_* env vars below remain honoured as a legacy compatibility layer (one notch above built-in defaults, below user/project), documented but no longer the recommended path. BROWX_WORKSPACE is the exception: it’s a location anchor (where the config store itself lives), not config.

Env varDefaultWhat
BROWX_WORKSPACE~/.browxai/Workspace root. All transient state (managed profile, logs, helper artefacts, config.json) lives here. NEVER cwd. See “no-trace contract” in the spec.
BROWX_ATTACH_CDP(unset)If set, attach to an externally-launched Chrome over CDP (BYOB). Loopback-only hostnames; the server refuses anything else. Attached browser is not-owned: the server never closes it or resets its storage on shutdown.
BROWX_HEADLESS0Managed-mode only. 1 or true (any case) launches headless.
BROWX_TEST_ATTRIBUTESdata-testid,data-test,data-cy,data-qaComma-separated list of HTML attributes treated as tier-1 selector anchors. Order-sensitive: the first match on a node wins. Add your codebase’s convention here (e.g. data-testid,data-type,data-test,data-cy) so it flows through snapshot() / find() / selectorHint / click({selector}) without code changes.
BROWX_CAPABILITIESread,navigation,action,humanComma-separated list of capability categories enabled at server start. The list replaces the default set, so BROWX_CAPABILITIES=eval enables eval alone and every other tool is disabled; write read,navigation,action,human,eval to add one. Off-by-default: eval (eval_js + poll_eval tools), byob-attach (every attached session: BROWX_ATTACH_CDP, the android and electron engines; refused with requiredCapability: "byob-attach" without it), network-body (full response bodies), clipboard (the shortcut tool’s OS-clipboard side-effect; observability still works without it), file-io (upload_file tool), secrets (per-session sensitive-data registry + egress masking), extensions (per-session unpacked-Chromium-extension management, headed + persistent only), self-approval (approve_actions), human-gate-override (lets set_permission_policy / set_fs_picker_policy / set_notification_policy move a policy off ask-human, and open_session reopen a name that held ask-human with a policy that leaves it; gates no tool), operator-channel (sends await_human, the confirm hooks and the permission / notification ask-human prompts to the host daemon’s socket; gates no tool, see BROWX_OPERATOR_SOCKET), live-view (lets the daemon on that socket start a screencast of a session; needs operator-channel, gates no tool, see Live view). A disabled tool returns a structured error on call. This is also the ceiling for a saved capabilities list, which can only narrow it.
BROWX_CONFIRM_REQUIREDnavigate_off_allowlist,byob_actionComma-separated list of policy hooks that route through await_human({kind:"confirm"}) before dispatch. Valid: navigate_off_allowlist, file_download, file_upload, byob_action.
BROWX_OPERATOR_SOCKET, BROWX_OPERATOR_TOKEN(unset)The host daemon’s Unix socket and the shared secret that authenticates it. Both are read once at start, removed from the environment, and used only when the off-by-default operator-channel capability is on; with the capability on and either missing the server refuses to start, and with it off both are ignored with a warning. The socket must be mode 0600 in a 0700 directory owned by the user running browxai, else the server refuses to start. The token must be at least 16 characters. Neither value appears in any log, error or tool result. See Threat model section 7.
BROWX_ALLOWED_ORIGINS(unset)Comma-separated allowlist for navigate. Each entry is a full origin with a scheme, and an entry without one stops the server at start. Wildcards allowed: https://*.example.com matches subdomains of any depth, not example.com itself. Off-allowlist navigations route through the confirm hook (if set) or proceed with a warning (if not). Defense-in-depth, not a security boundary; see threat model. An entry without a port matches the scheme’s default port only (80 for http, 443 for https); an entry with a port matches that port. navigate checks the landed URL too.
BROWX_BLOCKED_ORIGINS(unset)Comma-separated blocklist; overrides the allowlist. Entries match ports as for the allowlist: no port means the default port only.
BROWX_CREDENTIAL_ORIGINS(unset)Origins each get_credential account may be typed into, as acct=https://a.example|https://b.example,other=host: , between accounts, | between origins, each origin in the register_secret scope form. get_credential refuses an account that is not listed, before the vault is asked. An entry that does not parse is ignored with a startup warning. Read at start; restart after changing it.
BROWX_DISABLE_WEB_SECURITY(unset)1 launches managed/incognito sessions with SOP/CORS off (see disableWebSecurity). The only way to turn it on.
BROWX_PLUGINS(unset)Comma-separated plugin names a saved plugins config list may name. plugins.json (written by browxai plugin install) is unaffected.
BROWX_CONFIG_READONLY(unset)1 (or true) leaves set_config, reset_config and approve_actions out of tools/list and makes the config store refuse writes.
BROWX_DEFAULT_PROFILE(unset)Absolute path (or ~/…) of the workspace default profile directory, in place of <workspace>/profile: every persistent session with no profile launches on it, and so does browxai chrome start / chrome login. Checked at server start: a relative path, the filesystem root, the home directory itself, a symlink, a non-directory and a directory owned by another user all fail the start. A missing directory is created with mode 0700; an existing one readable by group or others gets a warning. Named profiles stay under <workspace>/profiles/, profile_snapshot / profile_restore use it for the default profile, and profile_status does not follow it.
BROWX_CHANNEL(unset)Playwright browser channel for chromium sessions (chrome, msedge, chrome-beta, chromium, …). Unset launches the installed Google Chrome, or the bundled Chromium when it is not installed; chromium pins the bundled build. See the channel config key above. Firefox’s own channel is BROWX_FIREFOX_CHANNEL.
BROWX_AUTOMATION_SWITCH(unset)1 / true restores Playwright’s --enable-automation switch on chromium launches. By default browxai drops it, so Chrome runs as a regular browser outside its automation-controlled mode. Set it to debug something that depends on that mode; nothing else about the launch changes.