A Bun-native, drop-in command-compatible alternative to Microsoft playwright-cli for AI agents. Same commands, same flag syntax, same snapshot YAML — replace playwright with bowser and existing playwright-cli skills work unchanged.
Built on Bun.WebView (new in Bun 1.3.12), so on macOS there's nothing to install beyond Bun itself, and on Linux / Windows it drives any installed Chrome / Chromium / Edge over the DevTools Protocol.
What sets it apart from playwright-cli:
- Bun-native. Single static binary via
bun build --compile. Fast cold start. No Node / npm / Playwright install dance. - Token-efficient. Capabilities are shell commands, not MCP tool schemas. A skill description of a few hundred tokens covers the whole API.
- Persistent sessions. Each named session keeps a long-lived browser process so multi-step flows survive between commands.
# From npm (requires Bun ≥ 1.3.12 on your PATH)
npm install -g @drakulavich/bowser-cli
# ...or directly from source
git clone https://github.com/drakulavich/bowser.git
cd bowser
bun install
bun link # exposes `bowser` on $PATHThen fetch a headless Chromium into Bowser's own cache (skipped if a system Chromium is already available):
bowser installPrebuilt single-file binaries for Linux (x64/arm64) and macOS (arm64/x64) are also attached to every GitHub Release — see Releases.
Requires Bun ≥ 1.3.12 for the npm/source install.
On macOS, bowser uses the native WKWebView engine by default — nothing to install.
It switches to Chrome/Chromium automatically if you opted in by running
bowser install (which caches a headless Chromium under ~/.bowser/chromium) or by
setting BOWSER_CHROMIUM_PATH. On Linux and Windows it always uses Chrome/Chromium.
Override the choice with BOWSER_BACKEND:
| Value | Effect |
|---|---|
BOWSER_BACKEND=webkit |
Force native WebKit (macOS only; errors elsewhere). |
BOWSER_BACKEND=chrome |
Force Chrome/Chromium. |
Screenshots are written as PNG files. bowser screenshot --filename out.png writes
to out.png (relative paths resolve against your current directory); without
--filename it writes screenshot-<session>.png, auto-incrementing (-1, -2, …)
if that file already exists. Captures are full-page (element-bounded screenshots are
not supported yet).
Bowser looks for a Chromium/Chrome binary in this order and uses the first one found:
$BOWSER_CHROMIUM_PATH(explicit override)~/.bowser/chromium/...(populated bybowser install)- System-wide installs:
/usr/bin/chromium-headless-shell,/usr/bin/chromium,/usr/bin/chromium-browser,/usr/bin/google-chrome,/Applications/Google Chrome.app/...,/Applications/Chromium.app/...
If none of those exist, run bowser install. It uses Playwright's downloader under the hood but writes into Bowser's own cache — it won't touch your Playwright setup. Use bowser install --force to re-download even when a system Chrome is already present.
bowser open https://example.com # navigate, save state
bowser snapshot # aria-tree YAML with [ref=eN]
bowser click e3 # click a ref
bowser fill e5 "hello@bowser.dev" # fill a form field
bowser press Enter # submit
bowser screenshot --filename=shot.png # capture
bowser close # end sessionEach session runs one persistent browser process (spawned lazily on first command, addressed over a Unix socket). Commands attach, run, and detach — so typed text, modals, dynamic DOM, cookies, and auth all survive across invocations. Session state lives under ~/.bowser/sessions/<name>/.
bowser -s=login open https://app.example.com/login
bowser -s=login fill e1 "me@example.com"
bowser -s=login fill e2 "$PASSWORD"
bowser -s=login click e3bowser --json snapshot | jq '.refs[] | select(.role == "button")'| Command | Description |
|---|---|
install [--force] |
Download a headless Chromium |
open [url] |
Start session; navigate if URL given |
goto <url> |
Navigate within current session |
snapshot [--filename=f] [--depth=N] |
aria-tree YAML of interactive refs; --depth=N clips landmark nesting (N=1 is flat, default is unbounded) |
click <ref> |
Click an element |
fill <ref> <text> |
Focus, clear, type |
type <text> |
Type into focused element |
press <key> |
Press a keyboard key |
hover <ref> |
Hover an element |
select <ref> <value> |
Choose a <select> option |
check <ref> / uncheck <ref> |
Toggle a checkbox |
screenshot [--filename=f] |
Full-page screenshot (PNG) |
resize <width> <height> |
Set the viewport size in pixels. Works on both backends. |
go-back / go-forward / reload |
Navigation |
list |
List sessions |
close [name] |
End a session (defaults to --session; positional name overrides) |
close --all |
Close every open session |
localstorage-list |
List all localStorage entries (key=value per line, or JSON with --json) |
localstorage-get <key> |
Read a localStorage value |
localstorage-set <key> <value> |
Write a localStorage entry |
localstorage-delete <key> |
Remove a localStorage entry |
localstorage-clear |
Clear all localStorage entries |
sessionstorage-list |
List all sessionStorage entries (key=value per line, or JSON with --json) |
sessionstorage-get <key> |
Read a sessionStorage value |
sessionstorage-set <key> <value> |
Write a sessionStorage entry |
sessionstorage-delete <key> |
Remove a sessionStorage entry |
sessionstorage-clear |
Clear all sessionStorage entries |
eval <expression> |
Evaluate a JS expression in the current page; prints the result |
run-code <code> |
Run multi-statement JS in the current page; wrap in an IIFE, use return to produce a value |
cookie-list [--domain=<d>] [--url=<u>] |
List cookies for the current page (or specified scope). HttpOnly cookies are first-class. Requires the chrome backend. |
cookie-get <name> [--domain=<d>] [--url=<u>] |
Print a cookie's value (empty if not found). HttpOnly cookies are visible. Requires the chrome backend. |
cookie-set <name> <value> [--domain=<d>] [--url=<u>] [--path=<p>] [--http-only] [--secure] [--same-site=Lax|Strict|None] [--expires=<unix-s>] |
Set a cookie. Defaults URL to current page. --http-only sets the HttpOnly flag. Requires the chrome backend. |
cookie-delete <name> [--domain=<d>] [--url=<u>] [--path=<p>] |
Delete a cookie. Requires the chrome backend. |
cookie-clear |
Wipe all browser cookies in this session. Requires the chrome backend. |
state-save <file> |
Dump the cookie jar + current-origin localStorage to a Playwright-compatible storageState JSON file. Requires the chrome backend. |
state-load <file> |
Restore cookies + localStorage from a storageState file. localStorage restores for origins matching the current page; others are reported skipped. Requires the chrome backend. |
mcp |
Run a Model Context Protocol stdio server exposing every command above as an MCP tool. |
Global flags: -s=<name> / --session=<name>, --json, -h/--help.
bowser mcp runs a Model Context Protocol server over stdio, exposing every browser command as an MCP tool — so MCP clients (Claude Desktop, etc.) can drive the browser without shelling out. Each tool maps 1:1 to a CLI command and takes an optional session argument; outputs are the same JSON as --json mode.
Register it in an MCP client config (e.g. claude_desktop_config.json):
{
"mcpServers": {
"bowser": { "command": "bowser", "args": ["mcp"] }
}
}Notes:
- Run
bowser installonce first (it is intentionally not exposed as a tool — it shells out and downloads Chromium). - The server is a thin client of the same per-session daemons the CLI uses; the first tool call on a fresh session spawns one.
- The protocol is hand-rolled (newline-delimited JSON-RPC) with zero runtime dependencies.
bowser openspawns a per-session daemon holding aBun.WebView, navigates, and saves{url, title}to~/.bowser/sessions/<name>/state.json.bowser snapshotruns a snapshot script in the page that walks the DOM, picks interactive elements, computes a stable CSS path (#idwhen safe, otherwise annth-of-typechain), and returns the refs as aria-tree YAML. Refs are persisted so later commands can resolvee3→html > body > button:nth-of-type(2).bowser click e3resolves the ref from state and dispatches the click via the daemon, usingBun.WebView's built-in actionability auto-wait — no polling, no hard-coded timeouts.
Because selectors are stable paths (not injected data- attributes), they survive page reloads between commands.
| Variable | Effect |
|---|---|
BOWSER_BACKEND |
webkit or chrome — override the auto-selected browser backend. |
BOWSER_CHROMIUM_PATH |
Explicit path to a chrome-headless-shell binary; bypasses auto-detection. |
BOWSER_OP_TIMEOUT_MS |
Per-operation timeout in milliseconds (default 30000; 0 disables). Bounds a wedged daemon operation — if the browser hangs, the command exits with a timeout error instead of blocking forever. |
bun test # unit + command tests with a fake daemon
BOWSER_E2E=1 bun test # + end-to-end against real headless Chromium
BOWSER_E2E=1 BOWSER_E2E_NET=1 bun test # + live-internet e2e (GitHub search)End-to-end examples included:
tests/e2e.test.ts— open/snapshot/click on adata:URL (no network)tests/e2e-todo.test.ts— a local todo app served byBun.serve: add three todos, toggle one, clear completed. Proves the daemon keeps state across commands.tests/e2e-search.test.ts— live web: search GitHub for OpenClaw, find the repo link, type into the search box and press Enter.
bun build src/cli.ts --compile --outfile dist/bowser
./dist/bowser open https://example.comCross-compile for other platforms:
bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/bowser-macos-arm64
bun build src/cli.ts --compile --target=bun-linux-x64 --outfile dist/bowser-linux-x64
bun build src/cli.ts --compile --target=bun-windows-x64 --outfile dist/bowser.exe- Persistent session daemon over Unix socket
- playwright-cli command compatibility for the core agent loop
- Snapshot nesting honoring
--depth=N - Storage commands (
cookie-*,localstorage-*,state-save/load)-
localstorage-{list,get,set,delete,clear} -
sessionstorage-{list,get,set,delete,clear} -
cookie-{list,get,set,delete,clear}— HttpOnly cookies are first-class; usesBun.WebView.cdp()(chrome backend only; see design) -
state-save/state-load— Playwright-compatiblestorageStateJSON (cookies + per-origin localStorage; chrome backend only)
-
- Tab management (
tab-list/tab-new/tab-select/tab-close) - Network mocking (
route,unroute) - Tracing / video / PDF output
-
eval,run-code -
resize -
dialog-accept/dismiss - MCP bridge subcommand for non-CLI clients (
bowser mcp) - Agent skill published to agentskills.io
The 0.2.0 release is a clean break: snap → snapshot, @e3 → e3, --session → -s=, session list → list. See CHANGELOG.md for the full migration table.
MIT