Control your browser via CDP. Extension + CLI, no wrapper layer.
Other Browser Automation spawn a fresh Chrome — no logins, no extensions, instantly flagged by bot detectors, double the memory. RunBrowser connects to your running browser instead. One Chrome extension, full CDP access, everything you're already logged into.
| Playwright MCP | RunBrowser | |
|---|---|---|
| Browser | Spawns new Chrome | Uses your Chrome |
| Extensions | None | Your existing ones |
| Login state | Fresh | Already logged in |
| Bot detection | Always detected | Can bypass (disconnect extension) |
| Collaboration | Separate window | Same browser as user |
# 1. Install the CLI
npm i -g @jiweiyuan/runbrowser
# 2. Load extension: chrome://extensions/ → Developer mode → Load unpacked → packages/extension/dist
# 3. Click the extension icon on a tab — it turns green
# 4. Use it
runbrowser status
runbrowser tab new https://example.com
runbrowser eval 'document.title'
runbrowser cdp Accessibility.getFullAXTree | jq '.nodes[] | select(.role.value=="button")'runbrowser skill install # → ./.claude/skills and ./.agents/skills
runbrowser skill install --global # → ~/.claude/skills and ~/.agents/skillsInstalls into the current project by default, so the skill is committed and
reviewed alongside the code it is used on; --global (-g) installs into
$HOME instead. Re-running is safe, and a SKILL.md you have edited yourself
is never overwritten — remove it first if you want ours back. runbrowser skill uninstall removes only files we installed.
This teaches your agent the CDP patterns that matter: read the accessibility tree before acting, filter it before printing, poll instead of sleeping.
RunBrowser uses the Chrome DevTools Protocol (CDP) directly. The extension bridges CDP commands over WebSocket to your running browser.
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
│ BROWSER │ │ LOCALHOST │ │ CLIENT │
│ │ │ │ │ │
│ ┌───────────────┐ │ │ WebSocket Server │ │ ┌───────────┐ │
│ │ Extension │<───────┬───> :19988 │ │ │ CLI / MCP │ │
│ └───────┬───────┘ │ WS │ │ │ └───────────┘ │
│ │ │ │ /extension │ │ │ │
│ chrome.debugger │ │ │ │ │ v │
│ v │ │ v │ │ ┌────────────┐ │
│ ┌───────────────┐ │ │ /cdp/:id <───────────────>│ │ CDP API │ │
│ │ Tab 1 (green) │ │ └──────────────────────┘ WS │ └────────────┘ │
│ │ Tab 2 (green) │ │ │ │ │
│ │ Tab 3 (gray) │ │ Tab 3 not controlled │ Playwright API │
└─────────────────────┘ (extension not clicked) └─────────────────┘
- No new Chrome instances — works with your current browser session
- No CDP mode required — no need to restart Chrome with special flags
- Full CDP access — complete Chrome DevTools Protocol capabilities
- Visual feedback — extension icon changes color (green = connected, gray = inactive)
Two commands touch a page. Everything you might expect as a verb — click, type,
read, screenshot, wait — is a CDP method, so it goes through cdp.
runbrowser cdp <Method> [params-json] # the page API
runbrowser eval '<js>' # shorthand for Runtime.evaluate
runbrowser tab list|new|switch|close # which target you're bound to
runbrowser status # is a browser attached
runbrowser session new|list|delete # isolated state, one per agentWhy no click/snapshot/@ref layer: every wrapper is an abstraction someone
decided the model needs, and it becomes a constraint the model has to work
around. Chrome's protocol is complete and documented, and LLMs were trained on
it. The wrapper layer was removed rather than maintained.
Navigation and screenshots are single CDP methods. Clicking and typing are short
CDP sequences — runbrowser skill documents them, including the parts a click
verb used to hide (scroll into view, no actionability check, keyUp pairing).
runbrowser cdp Page.navigate '{"url":"https://example.com"}'
runbrowser cdp Accessibility.getFullAXTree | jq '.nodes[] | select(.role.value=="button")'
runbrowser cdp Input.dispatchMouseEvent '{"type":"mousePressed","x":420,"y":310,"button":"left","clickCount":1}'
runbrowser cdp Page.captureScreenshot | jq -r .data | base64 -d > shot.pngBoth cdp and eval accept their payload on stdin, which is easier than
quoting multi-line JS in a shell:
runbrowser eval <<'JS'
Array.from(document.querySelectorAll('a')).map(a => a.href)
JSUse the accessibility tree, not screenshots — it is text, so you can filter it
with jq, and it costs a fraction of the tokens. Filter before printing; a real
page is thousands of nodes.
runbrowser cdp Accessibility.getFullAXTree \
| jq '.nodes[] | select(.role.value=="button") | {name: .name.value, id: .backendDOMNodeId}'Tabs are shared across sessions; session state is not. Several agents can work in one browser without colliding.
runbrowser tab new https://example.com # opens and binds to it
runbrowser tab list # → marks the bound tab
runbrowser session new # → prints an id
runbrowser -s 3 tab list # act inside session 3Site commands from the runbrowser/commands repo are unchanged — they are a different audience from the agent CDP path.
runbrowser commands list
runbrowser commands install redditrunbrowser config set <key> <value> # set token, host, etc.
runbrowser config unset <key> # remove a config value
runbrowser config show # show current config
runbrowser logfile # print log file paths
runbrowser skill # print full agent instructionsThe CLI is the recommended way to use RunBrowser. For MCP server integration:
{
"mcpServers": {
"runbrowser": {
"command": "npx",
"args": ["-y", "@jiweiyuan/runbrowser-mcp@latest"]
}
}
}MCP tools: cdp, eval, tab, status, skill, command.
For full MCP instructions, see MCP.md.
Control Chrome on a remote machine — headless Mac mini, cloud VM, devcontainer:
# On the host machine
runbrowser serve --host 0.0.0.0 --token <secret>
# From anywhere
export RUNBROWSER_HOST=192.168.1.10
export RUNBROWSER_TOKEN=<secret>
runbrowser navigate https://example.com -s 1For Docker/devcontainers, use RUNBROWSER_HOST=host.docker.internal.
The relay exposes a standard CDP WebSocket endpoint for Playwright:
import { chromium } from 'playwright-core'
import { startRunBrowserCDPRelayServer, getCdpUrl } from '@jiweiyuan/runbrowser-server'
const server = await startRunBrowserCDPRelayServer()
const browser = await chromium.connectOverCDP(getCdpUrl())
const page = browser.contexts()[0].pages()[0]
await page.goto('https://example.com')
await page.screenshot({ path: 'screenshot.png' })
// Don't call browser.close() — it closes the user's Chrome
server.close()The CLI and MCP use CDP directly and do not require
playwright-core.
| Variable | Description |
|---|---|
RUNBROWSER_HOST |
Remote relay server host |
RUNBROWSER_TOKEN |
Authentication token |
RUNBROWSER_PORT |
Relay server port (default: 19988) |
RUNBROWSER_SESSION |
Default session ID (avoids -s flag) |
RUNBROWSER_AUTO_ENABLE |
Auto-create tab on connect |
RUNBROWSER_LOG_FILE_PATH |
Custom path for relay server log file |
RUNBROWSER_CDP_LOG_FILE_PATH |
Custom path for CDP JSONL log file |
packages/
├── cli/ # @jiweiyuan/runbrowser — CLI
├── server/ # @jiweiyuan/runbrowser-server — WebSocket relay, CDP bridge, site commands
├── mcp/ # @jiweiyuan/runbrowser-mcp — MCP server (thin HTTP wrapper)
├── extension/ # Chrome extension (chrome.debugger ↔ WebSocket)
├── e2e/ # End-to-end tests
├── website/ # Next.js + next-intl marketing site (en/zh/ja/fr/es)
└── vite-plugin-extension-reload/ # Dev tool for extension hot reload
| Icon | Meaning |
|---|---|
| Gray | Not connected to any tab |
| Green | Connected and ready |
| Orange badge (...) | Connecting to relay server |
| Red badge (!) | Error occurred |
- Local only — WebSocket server binds to
localhost:19988 - Origin validation — only the RunBrowser extension origin is accepted
- Explicit consent — only tabs where you clicked the extension icon
- Visible automation — Chrome shows an automation banner on controlled tabs
runbrowser logfile # prints log file paths
# relay: ~/.runbrowser/relay-server.log
# cdp: ~/.runbrowser/cdp.jsonl| Problem | Fix |
|---|---|
| Extension icon stays gray | Click it again. Check chrome://extensions/ for errors. |
| "Extension not connected" | Click extension icon on at least one tab. |
All pages return about:blank |
Restart Chrome (known Chrome bug). |
| Port 19988 in use | lsof -ti :19988 | xargs kill |
RunBrowser wouldn't exist without the work of these projects and their maintainers.
- playwriter by Tommaso De Rossi — The project that started it all. RunBrowser began as a fork of playwriter and owes its Chrome extension architecture to Tommaso's original design.
- bb-browser & bb-sites — A beautifully designed browser automation tool with an impressive collection of 45+ community site adapters. The bb-sites ecosystem is a constant source of inspiration.
- agent-browser by Vercel — Pioneered many ideas around comprehensive browser CLIs for AI agents.
- pi by Mario Zechner — The command extensions system (
runbrowser commands install/list/uninstall) was inspired by pi's elegant approach to extensibility and package management.
Thank you to all these maintainers for pushing the ecosystem forward.
MIT