A Chrome extension and CLI that let your agents control your actual browser with logins, extensions, and cookies already there. No headless instance, no bot detection, no extra memory. Star on GitHub.

Playwriter controlling Chrome with accessibility labels overlay

Your existing Chrome session. Extensions, logins, cookies all there.

Other browser MCPs either spawn a fresh Chrome or give agents a fixed set of tools. New Chrome means no logins, no extensions, instant bot detection, and double the memory. Fixed tools mean the agent can't profile performance, can't set breakpoints, can't intercept network requests it can only do what someone decided to expose.

Playwriter gives agents the full Playwright API through a single execute tool. One tool, any Playwright code, no wrappers. Low context usage because there's no schema bloat from dozens of tool definitions. And it runs in your existing browser, so nothing extra gets spawned.

Getting started

Four steps and your agent is browsing.

  1. Install the Chrome extension
  2. Click the extension icon on a tab it turns green
  3. Install the CLI:
npm i -g playwriter

Then install the skill it teaches your agent how to use Playwriter: which selectors to use, how to avoid timeouts, how to read snapshots, and all available utilities.

npx -y skills add remorses/playwriter

The extension connects your browser to a local WebSocket relay on localhost:19988. The CLI sends Playwright code through the relay. No remote servers, no accounts, nothing leaves your machine.

playwriter session new # new sandbox, outputs id (e.g. 1) playwriter -e "page.goto('https://example.com')" playwriter -e "snapshot({ page })" playwriter -e "page.locator('aria-ref=e5').click()"

Extension icon green = connected. Gray = not attached to this tab.

How it works

Click the extension icon on a tab it attaches via chrome.debugger and opens a WebSocket to a local relay. Your agent (CLI, MCP, or a Playwright script) connects to the same relay. CDP commands flow through; the extension forwards them to Chrome and sends responses back. No Chrome restart, no flags, no special setup.

┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐ │ BROWSER │ │ LOCALHOST │ │ CLIENT │ │ │ │ │ │ │ │ ┌───────────────┐ │ │ WebSocket Server │ │ ┌───────────┐ │ │ │ Extension │<───────┬───> :19988 │ │ │ CLI / MCP │ │ │ └───────┬───────┘ │ WS │ │ │ └───────────┘ │ │ │ │ │ /extension │ │ │ │ │ chrome.debugger │ │ │ │ │ v │ │ v │ │ v │ │ ┌────────────┐ │ │ ┌───────────────┐ │ │ /cdp/:id <───────────────>│ │ execute │ │ │ │ Tab 1 (green) │ │ └──────────────────────┘ WS │ └────────────┘ │ │ │ Tab 2 (green) │ │ │ │ │ │ │ Tab 3 (gray) │ │ Tab 3 not controlled │ Playwright API │ └─────────────────────┘ (extension not clicked) └─────────────────┘

The relay multiplexes sessions, so multiple agents or CLI instances can work with the same browser at the same time.

Collaboration

Because the agent works in your browser, you can collaborate. You see everything it does in real time. When it hits a captcha, you solve it. When a consent wall appears, you click through it. When the agent gets stuck, you disable the extension on that tab, fix things manually, re-enable it, and the agent picks up where it left off.

You're not watching a remote screen or reading logs after the fact. You're sharing a browser the agent does the repetitive work, you step in when it needs a human.

Accessibility snapshots

Your agent needs to see the page before it can act. Accessibility snapshots return every interactive element as text, with Playwright locators attached. 520KB instead of 100KB+ for a screenshot cheaper, faster, and the agent can parse them without vision.

playwriter -e "snapshot({ page })" # Output: # - banner: # - link "Home" [id="nav-home"] # - navigation: # - link "Docs" [data-testid="docs-link"] # - link "Blog" role=link[name="Blog"]

Each line ends with a locator you can pass directly to page.locator(). Subsequent calls return a diff, so you only see what changed. Use search to filter large pages.

# Search for specific elements playwriter -e "snapshot({ page, search: /button|submit/i })" # Always print URL first, then snapshot — pages can redirect playwriter -e "console.log('URL:', page.url()); snapshot({ page }).then(console.log)"

Use snapshots as the primary way to read pages. Only reach for screenshots when spatial layout matters grids, dashboards, maps.

Visual labels

When the agent needs to understand where things are on screen, screenshotWithAccessibilityLabels overlays Vimium-style labels on every interactive element. The agent sees the screenshot, reads the labels, and clicks by reference.

playwriter -e "screenshotWithAccessibilityLabels({ page })" # Returns screenshot + accessibility snapshot with aria-ref selectors playwriter -e "page.locator('aria-ref=e5').click()"

Labels are color-coded by element type: yellow for links, orange for buttons, coral for inputs, pink for checkboxes, peach for sliders, salmon for menus, amber for tabs. The ref system is shared with snapshot(), so you can switch between text and visual modes freely.

Vimium-style labels. Screenshot + snapshot in one call.

Sessions

Run multiple agents at once without them stepping on each other. Each session is an isolated sandbox with its own state object. Variables, pages, and listeners persist between calls. Browser tabs are shared, but state is not.

playwriter session new # => 1 playwriter session new # => 2 playwriter session list # shows sessions + state keys # Session 1 stores data playwriter -s 1 -e "state.users = page.$$eval('.user', els => els.map(e => e.textContent))" # Session 2 can't see it playwriter -s 2 -e "console.log(state.users)" # undefined

Create your own page to avoid interference from other agents. Reuse an existing about:blank tab or create a fresh one, and store it in state.

playwriter -s 1 -e "state.myPage = context.pages().find(p => p.url() === 'about:blank') ?? context.newPage(); state.myPage.goto('https://example.com')" # All subsequent calls use state.myPage playwriter -s 1 -e "state.myPage.title()"

Debugger & editor

Things no other browser MCP can do. Set breakpoints, step through code, inspect variables at runtime. Live-edit page scripts and CSS without reloading. Full Chrome DevTools Protocol access, not a watered-down subset.

# Set breakpoints and debug playwriter -e "state.cdp = getCDPSession({ page }); state.dbg = createDebugger({ cdp: state.cdp }); state.dbg.enable()" playwriter -e "state.scripts = state.dbg.listScripts({ search: 'app' }); state.scripts.map(s => s.url)" playwriter -e "state.dbg.setBreakpoint({ file: state.scripts[0].url, line: 42 })" # Live edit page code playwriter -e "state.editor = createEditor({ cdp: state.cdp }); state.editor.enable()" playwriter -e "state.editor.edit({ url: 'https://example.com/app.js', oldString: 'const DEBUG = false', newString: 'const DEBUG = true' })"

Edits are in-memory and persist until the page reloads. Useful for toggling debug flags, patching broken code, or testing quick fixes without touching source files. The editor also supports grep across all loaded scripts.

Breakpoints, stepping, variable inspection from the CLI.

Network interception

Let the agent watch network traffic to reverse-engineer APIs, scrape data behind JavaScript rendering, or debug failing requests. Captured data lives in state and persists across calls.

# Start intercepting playwriter -e "state.responses = []; page.on('response', async res => { if (res.url().includes('/api/')) { try { state.responses.push({ url: res.url(), status: res.status(), body: await res.json() }); } catch {} } })" # Trigger actions, then analyze playwriter -e "page.click('button.load-more')" playwriter -e "console.log('Captured', state.responses.length, 'API calls'); state.responses.forEach(r => console.log(r.status, r.url.slice(0, 80)))" # Replay an API call directly playwriter -e "page.evaluate(async (url) => { const res = await fetch(url); return res.json(); }, state.responses[0].url)"

Faster than scraping the DOM. The agent captures the real API calls, inspects their schemas, and replays them with different parameters. Works for pagination, authenticated endpoints, and anything behind client-side rendering.

Screen recording

Have the agent record what it's doing as MP4 video. The recording uses chrome.tabCapture and runs in the extension context, so it survives page navigation.

# Start recording playwriter -e "startRecording({ page, outputPath: './recording.mp4', frameRate: 30 })" # Navigate, interact — recording continues playwriter -e "page.click('a'); page.waitForLoadState('domcontentloaded')" playwriter -e "page.goBack()" # Stop and save playwriter -e "stopRecording({ page })"

Unlike getDisplayMedia, this approach persists across navigations because the extension holds the MediaRecorder, not the page. You can also check recording status with isRecording or cancel without saving with cancelRecording.

Native tab capture. 3060fps. Survives navigation.

Comparison

Why use this over the alternatives.

vs Playwright MCP
Playwright MCPPlaywriter
BrowserSpawns new ChromeUses your Chrome
ExtensionsNoneYour existing ones
Login stateFreshAlready logged in
Bot detectionAlways detectedCan bypass
CollaborationSeparate windowSame browser as user
vs BrowserMCP
BrowserMCPPlaywriter
Tools12+ dedicated tools1 execute tool
APILimited actionsFull Playwright
Context usageHigh (tool schemas)Low
LLM knowledgeMust learn toolsAlready knows Playwright
vs Claude Browser Extension
Claude ExtensionPlaywriter
Agent supportClaude onlyAny MCP client
Windows WSLNoYes
Context methodScreenshots (100KB+)A11y snapshots (5–20KB)
Playwright APINoFull
DebuggerNoYes
Live code editingNoYes
Network interceptionLimitedFull
Raw CDP accessNoYes

Remote access

Control Chrome on a remote machine a headless Mac mini, a cloud VM, a devcontainer. A traforo tunnel exposes the relay through Cloudflare. No VPN, no firewall rules, no port forwarding.

# On the host machine — start relay with tunnel npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token <secret> # From anywhere — set env vars and use normally export PLAYWRITER_HOST=https://my-machine-tunnel.traforo.dev export PLAYWRITER_TOKEN=<secret> playwriter -e "page.goto('https://example.com')"

Also works on a LAN without tunnels just set PLAYWRITER_HOST=192.168.1.10. Works for MCP too set PLAYWRITER_HOST and PLAYWRITER_TOKEN in your MCP client env config. Use cases: headless Mac mini, remote user support, multi-machine automation, dev from a VM or devcontainer.

Security

Everything runs on your machine. The relay binds to localhost:19988 and only accepts connections from the extension. No remote server, no account, no telemetry.

  • Local only WebSocket server binds to localhost. Nothing leaves your machine.
  • Origin validation only the Playwriter extension origin is accepted. Browsers cannot spoof the Origin header, so malicious websites cannot connect.
  • Explicit consent only tabs where you clicked the extension icon are controlled. No background access.
  • Visible automation Chrome shows an automation banner on controlled tabs.