Skip to content

Repository files navigation

browserwright

Let an AI/code agent drive a real (or isolated) Chrome from the terminal over CDP — open pages, click, type, fill forms, scrape, screenshot — and author userscripts the browser runs on matching sites.

One installable package, two CLIs that work together:

  • browserwright-daemon (Layer 1) — resolves a Chrome CDP WebSocket URL and proxies it. Backends: env / cdp / extension. Also launch-chrome to spawn an isolated Chrome.
  • browserwright (Layer 2) — the agent-facing surface: sessions, heredoc scripting with pre-imported primitives, reusable site tasks, memory, and userscript management.
.
├── src/browserwright/        the package
│   ├── …                     Layer 2 — sessions / primitives / site skills / memory / userscripts
│   └── daemon/               Layer 1 — CDP URL resolver + proxy (env/cdp/extension backends)
├── chrome-extension/         unpacked relay extension for the `extension` backend
├── skill/                    Agent skill bundle (symlinked into Claude Code, Codex, and Pi)
├── tests/{skill,daemon}/     test suites
└── docs/                     deeper docs (architecture.md, daemon.md, session-workspaces.md, …)

Contributing or hacking on this repo? Start at AGENTS.md — it points to ONBOARD.md (clone → install → test loop), docs/architecture.md (architecture orientation), TESTING.md (test-suite map), and RELEASING.md (releases + updating the global install). This README covers the end-user path only.

Prerequisites

  • macOS or Linux
  • Python 3.11 (brew install python@3.11 / pyenv install 3.11)
  • Chrome / Chromium (any flavor)
  • ~/.local/bin on $PATH
  • uv — manages the venv, lockfile, and Python toolchain (uv can fetch Python 3.11 itself)

Install

PyPI / global tool install

The distribution target is one PyPI package named browserwright. A global tool install should expose both CLIs:

pipx install browserwright
# or
uv tool install browserwright

After that, install the one global daemon service:

browserwright-daemon install
browserwright-daemon status
browserwright-daemon doctor

On macOS, browserwright-daemon install registers the daemon as a LaunchAgent at ~/Library/LaunchAgents/com.browserwright-daemon.plist, starts it on login, and keeps the single daemon socket at:

${XDG_RUNTIME_DIR:-/tmp}/browserwright-daemon.sock

Linux currently does not auto-install a service; run browserwright-daemon serve from your own systemd-user unit or process supervisor.

The browser extension is intentionally not part of the PyPI packaging contract: it ships through the Chrome Web Store and installs from there like any other extension (auto-updating, no Developer mode):

https://chromewebstore.google.com/detail/browserwright-daemon-rela/okgnalaalckoaeledbjhpjiccmcdceeb

Developers working on the extension itself can load the repo's chrome-extension/ directory unpacked (chrome://extensions → Developer mode → Load unpacked); the GitHub Release extension artifact and mise run upgrade-global are the fallback distribution path for machines that want a specific pinned version without the store.

The agent skill bundle remains a thin shell around the installed CLI. For PyPI installs, the intended stable contract is:

browserwright --print-skill

Agents should read the runtime guide generated by the installed package version, so the skill instructions stay version-locked to the CLI that actually runs.

The global install is considered healthy when these pass:

browserwright version check
browserwright-daemon version check
browserwright-daemon doctor

If the daemon was already running when you upgraded the global tool, restart it:

browserwright-daemon restart

If it is running in the foreground instead of as the macOS LaunchAgent:

browserwright-daemon stop
browserwright-daemon serve

Releases, upgrades, and local development

Cutting a release, updating a machine's global install (mise run upgrade-global), the extension reload flow, and the dev-link workflow are all documented in RELEASING.md and ONBOARD.md.

Smoke test

# Start an isolated Chrome (own profile dir, won't touch your daily Chrome)
browserwright-daemon launch-chrome --port 9333 --profile bs-smoke --persistent --json

# Drive it
BD_PORT=9333 BD_BACKEND=cdp browserwright <<'PY'
page.goto("https://example.com", wait_until="load")
print(f"URL:   {page.url}")
print(f"Title: {page.title()}")
PY
# expected:
#   URL:   https://example.com/
#   Title: Example Domain

Clean up: kill <pid> (the launch-chrome --json output includes the pid).

Usage

Sessions: create once, pass everywhere

A session is the isolation key that lets multiple agents drive browsers without colliding. Create one, then every later call carries its id (via --session or BD_SESSION):

sid=$(browserwright session new --backend=extension)
BD_SESSION=$sid browserwright <<'PY'
page.goto("https://news.ycombinator.com", wait_until="load")
print(page.title())
PY
browserwright whoami --session=$sid
browserwright session end --session=$sid

A bare heredoc with no session/BD_PORT context exits 2 with guidance — the daemon is never silently shared.

One global daemon serves every session (fixed socket browserwright-daemon.sock; no per-instance name). The session's backend is fixed at session new and never changes. On extension the session's "browser" is a Chrome tab group (named after the session) inside the user's real Chrome; session end closes the whole group. On cdp the daemon launches and owns a dedicated, isolated Chrome (profile bs-s<id>) that dies with the session. Isolation caveat: cdp sessions get isolated profiles (separate cookies/storage), but extension tab groups isolate only the tab set — all extension sessions share the user's one profile, so they share cookies/login/origin storage with each other and with the user.

Two invocation forms

# (a) Inline heredoc — one-off scripts; drive the injected Playwright `page`
BD_SESSION=$sid browserwright <<'PY'
page.goto("https://news.ycombinator.com", wait_until="load")
print(page.title())
print(snapshot())          # [ref=eN] aria tree → page.locator("aria-ref=eN")
PY

# (b) Solidified task — reusable, pre-saved flow under ~/.browserwright/site-skills/<host>/tasks/
browserwright list-tasks
browserwright list-tasks --query="search the web"
browserwright task wikipedia.org/lookup --title="Wikipedia"

Choose a backend

Scenario Backend How
Your daily Chrome (logged-in / personal) (default for "use my browser") extension browserwright session new --backend=extension … — load chrome-extension/ once, connect via the daemon's relay; zero popups
Scripts / iterative work in throwaway profiles cdp + isolated Chrome browserwright-daemon launch-chrome --port 9333 --profile bs-dev + BD_PORT=9333 BD_BACKEND=cdp
Fingerprint browser (AdsPower / MultiLogin / 比特浏览器) cdp point BD_PORT at the tool's exposed port
Externally-owned CDP endpoint (anti-detect / cloud browser) cdp attach browserwright session new --backend=cdp --attach=ws://… --name=… (attach-owned — never closed on session end). Repeat for as many profiles as you need; one daemon serves them all

Interactive wizard: browserwright install — walks the decision tree and writes your pick.

Scaling env to N profiles: one daemon has one shared upstream, so drive N external profiles with N isolated daemons — each with its own XDG_RUNTIME_DIR (distinct socket), --facade-port, and BD_CDP_WS, one env session apiece. See docs/session-workspaces.md §"Env Backend".

Reaching the facade from another machine (Tailscale/LAN): the Playwright facade binds 127.0.0.1 by default and is never exposed off-box unless you opt in. Pass --facade-host <tailnet-ip> (or BD_FACADE_HOST / facade_host in config.toml; 0.0.0.0 to bind all interfaces) and a remote client can connect_over_cdp("http://<tailnet-ip>:19990/cdp") — the facade's /json/version bootstrap rewrites the advertised webSocketDebuggerUrl from the request's Host header, so the ws URL points back at the address the client actually used. No auth is added, so only bind an interface you trust (a Tailscale IP is private to your tailnet).

Remote clients own one tab group each (extension backend, ADR-0010): a connection without ?session= is auto-scoped to its own private tab group — <label>-BWauto-<hex>, where ?label= sets the prefix (default anon). It can only see and operate the tabs in that group; the group is created on first use, closed when the connection drops (ws heartbeat + 15-min orphan reaper), and never reused across connections. Add ?label= so the group title tells you who owns it: connect_over_cdp("ws://<tailnet-ip>:19990/cdp?label=hermes").

Userscripts

Author Tampermonkey-style scripts the extension backend injects on matching sites:

browserwright userscript push ./greet.user.js --verify
browserwright userscript list
browserwright userscript toggle <id>
browserwright userscript logs <id>
browserwright userscript remove <id>

The heredoc surface

Browser driving is real synchronous Playwright. Every heredoc gets the following names injected, already connected through the daemon's Playwright CDP facade:

  • page — a Playwright Page bound to the session's current tab, reused across heredocs. Navigate it in place (page.goto, page.locator, page.fill, page.click, …); never page.close().
  • context — the Playwright BrowserContext. context.new_page() opens a real second tab (in the session's tab group on extension) — then goto() it; manage several tabs with the tabs() / switch_tab() primitives.
  • snapshot() — a first-party AI aria snapshot; each node carries a [ref=eN] you act on via page.locator("aria-ref=eN"). Prefer this over screenshots; re-snapshot() after each action (observe → act → observe).

Tabs are workstreams, not steps: keep a tab for a page you will return to (its state — scroll, form input, JS state — survives), navigate in place when you are passing through, and never close the browser/context (those are the user's real tabs). switch_tab("url-substring") moves the session's current tab (the one page is bound to) between open tabs.

Non-browser helpers (also pre-imported):

  • HTTP (no browser, for static pages): http_get(url)
  • Tabs: tabs, switch_tab (session tab management, see above)
  • Memory: remember, remember_global, remember_preference, memory_read
  • Site-skills / tasks: list_site_skills, load_site_skill, run_task, bootstrap_site

Full catalogue and guidance in skill/SKILL.md.

Diagnostics

browserwright-daemon doctor                  # which backends are live, why each is/isn't usable
browserwright-daemon status --json           # daemon liveness + endpoint + facade port
browserwright doctor                         # skill-side health

Agent integrations

The skill/ directory is an agent skill bundle. Release install symlinks it into Claude Code, Codex, and Pi skill directories, and each symlink points at the active immutable release copy. The bundle is a stable shell; it tells the agent to run browserwright --print-skill for the version-locked runtime guide. Prompts like "open example.com and screenshot it", "scrape the HN front page", or "write me a userscript that …" trigger it automatically.

Uninstall

rm ~/.local/bin/browserwright ~/.local/bin/browserwright-daemon
rm ~/.claude/skills/browserwright
rm ~/.codex/skills/browserwright
rm ~/.pi/agent/skills/browserwright
rm -rf .venv
rm -rf ~/.cache/browserwright-daemon ~/.browserwright

Further reading

  • AGENTS.md — entry point for contributors and code agents
  • docs/architecture.md — architecture tour + isolated local dev
  • docs/session-workspaces.md — the session workspace / isolation model
  • docs/daemon.md — backend internals, env vars, config.toml
  • docs/skill.md — the Layer 2 agent-facing surface
  • TESTING.md — map of the test suites and how to run them
  • docs/archive/browser-connection.mdwhy this stack exists (CDP discovery paths, Chrome 144+ popup mechanics)

License

GNU AGPL-3.0 — copyleft: modifications must stay open source, including when offered as a network service. If you build on this code, your derivative must be AGPL too. See LICENSE for the full terms.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages