
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.1npm i -g playwriter
1npx -y skills add remorses/playwriter
localhost:19988. The CLI sends Playwright code through the relay. No remote servers, no
accounts, nothing leaves your machine.1234playwriter 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()"
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) └─────────────────┘
12345678playwriter -e "snapshot({ page })" # Output: # - banner: # - link "Home" [id="nav-home"] # - navigation: # - link "Docs" [data-testid="docs-link"] # - link "Blog" role=link[name="Blog"]
page.locator().
Subsequent calls return a diff, so you only see what changed. Use search to
filter large pages.12345# Search for specific elements playwriter -e "snapshot({ page, search: /button|submit/i })" # Always print URL first, then snapshot playwriter -e "console.log('URL:', page.url()); snapshot({ page }).then(console.log)"
screenshotWithAccessibilityLabels overlays Vimium-style labels on every
interactive element. The agent sees the screenshot, reads the labels, and clicks by reference.1234playwriter -e "screenshotWithAccessibilityLabels({ page })" # Returns screenshot + accessibility snapshot with aria-ref selectors playwriter -e "page.locator('aria-ref=e5').click()"
snapshot(), so you can switch between text and visual modes freely.state object. Variables, pages, and listeners persist between
calls. Browser tabs are shared, but state is not.123456789playwriter 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
about:blank tab or create a fresh one, and store it in state.1234playwriter -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()"
12345678# 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' })"
grep across all loaded scripts.state and persists across calls.123456789# 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)"
chrome.tabCapture and runs in the extension context, so it
survives page navigation.123456789# 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 })"
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.| Playwright MCP | Playwriter | |
| Browser | Spawns new Chrome | Uses your Chrome |
| Extensions | None | Your existing ones |
| Login state | Fresh | Already logged in |
| Bot detection | Always detected | Can bypass |
| Collaboration | Separate window | Same browser as user |
| Playwright CLI | Playwriter | |
| Browser | Spawns new browser | Uses your Chrome |
| Login state | Fresh | Already logged in |
| Extensions | None | Your existing ones |
| Captchas | Always blocked | Bypass (disconnect extension) |
| Collaboration | Separate window | Same browser as user |
| Capabilities | Limited command set | Anything Playwright can do |
| Raw CDP access | No | Yes |
| Video recording | File-based tracing | Native tab capture (30-60fps) |
| BrowserMCP | Playwriter | |
| Tools | 12+ dedicated tools | 1 execute tool |
| API | Limited actions | Full Playwright |
| Context usage | High (tool schemas) | Low |
| LLM knowledge | Must learn tools | Already knows Playwright |
| Claude Extension | Playwriter | |
| Agent support | Claude only | Any MCP client |
| Windows WSL | No | Yes |
| Context method | Screenshots (100KB+) | A11y snapshots (5-20KB) |
| Playwright API | No | Full |
| Debugger | No | Yes |
| Live code editing | No | Yes |
| Network interception | Limited | Full |
| Raw CDP access | No | Yes |
1234567# 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')"
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.localhost:19988 and only
accepts connections from the extension. No remote server, no account, no telemetry.