+
+
+ Playwriter lets you control your Chrome browser with the full
+ Playwright API. A Chrome extension, a local relay, and a CLI. No new
+ browser windows, no Chrome flags, no context bloat.{" "}
+ Star on GitHub.
+
+
+
+

+
+
+ Your existing Chrome session. Extensions, logins, cookies — all there.
+
+
+
+ Every browser automation MCP I tried either spawns a new Chrome
+ instance or forces you into a limited set of predefined tools. Playwriter
+ does neither. It connects to the browser you already have open,
+ exposes the full Playwright API through a single{" "}
+ execute tool, and gets out of the way.
+ One tool. Any Playwright code. No wrappers.
+
+
+
+
+
+ Three steps. Extension, icon click, then you're automating.
+
+
+
+ -
+ Install the{" "}
+ Chrome extension
+
+ - Click the extension icon on a tab — it turns green
+ - Install CLI and run your first command:
+
+
+ {dedent`
+ npm i -g playwriter
+ playwriter -s 1 -e "await page.goto('https://example.com')"
+ `}
+
+
+ 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.
+
+
+ {dedent`
+ playwriter session new # new sandbox, outputs id (e.g. 1)
+ playwriter -s 1 -e "await page.goto('https://example.com')"
+ playwriter -s 1 -e "console.log(await snapshot({ page }))"
+ playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"
+ `}
+
+
+ Extension icon green = connected. Gray = not attached to this tab.
+
+
+
+
+
+
+
+ The extension uses chrome.debugger to
+ attach to tabs where you clicked the icon. It opens a WebSocket
+ connection to a local relay server. The CLI (or MCP, or your own
+ Playwright script) connects to the same relay. CDP commands flow
+ through; the extension forwards them to Chrome and sends responses
+ back.
+
+
+ {dedent`
+ ┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
+ │ 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) └─────────────────┘
+ `}
+
+
+ No Chrome restart required. No --remote-debugging-port{" "}
+ flags. The extension handles the CDP attachment transparently, and
+ the relay multiplexes sessions so multiple agents or CLI instances
+ can work with the same browser simultaneously.
+
+
+
+
+
+
+
+ The core feedback loop is observe → act → observe.
+ Accessibility snapshots are the primary way to read page state. They return
+ the full interactive element tree as text, with Playwright locators attached
+ to every element.
+
+
+ {dedent`
+ playwriter -s 1 -e "await 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.
+
+
+ {dedent`
+ # Search for specific elements
+ playwriter -s 1 -e "await snapshot({ page, search: /button|submit/i })"
+
+ # Always print URL first, then snapshot — pages can redirect
+ playwriter -s 1 -e "console.log('URL:', page.url()); await snapshot({ page }).then(console.log)"
+ `}
+
+
+ Snapshots are text. They cost a fraction of what screenshots cost in
+ tokens. Use them as your primary debugging tool. Only reach for
+ screenshots when spatial layout matters — grids, dashboards, maps.
+
+
+
+ Accessibility tree as text. 5–20KB vs 100KB+ for screenshots.
+
+
+
+
+
+
+
+ For pages where spatial layout matters,{" "}
+ screenshotWithAccessibilityLabels overlays{" "}
+ Vimium-style labels on every interactive element. Take a screenshot,
+ read the labels, click by reference.
+
+
+ {dedent`
+ playwriter -s 1 -e "await screenshotWithAccessibilityLabels({ page })"
+ # Returns screenshot + accessibility snapshot with aria-ref selectors
+
+ playwriter -s 1 -e "await 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.
+
+
+
+
+
+
+
+ Each session runs in an isolated sandbox with its own{" "}
+ state object. Variables, pages, listeners
+ persist between calls within a session. Different sessions get
+ different state. Browser tabs are shared.
+
+
+ {dedent`
+ 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 = await 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.
+
+
+ {dedent`
+ playwriter -s 1 -e "state.myPage = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage(); await state.myPage.goto('https://example.com')"
+
+ # All subsequent calls use state.myPage
+ playwriter -s 1 -e "console.log(await state.myPage.title())"
+ `}
+
+
+
+
+
+
+ Full Chrome DevTools Protocol access. Set breakpoints, step through
+ code, inspect variables at runtime. Live-edit page scripts and CSS
+ without reloading.
+
+
+ {dedent`
+ # Set breakpoints and debug
+ playwriter -s 1 -e "state.cdp = await getCDPSession({ page }); state.dbg = createDebugger({ cdp: state.cdp }); await state.dbg.enable()"
+ playwriter -s 1 -e "state.scripts = await state.dbg.listScripts({ search: 'app' }); console.log(state.scripts.map(s => s.url))"
+ playwriter -s 1 -e "await state.dbg.setBreakpoint({ file: state.scripts[0].url, line: 42 })"
+
+ # Live edit page code
+ playwriter -s 1 -e "state.editor = createEditor({ cdp: state.cdp }); await state.editor.enable()"
+ playwriter -s 1 -e "await 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.
+
+
+
+
+
+
+
+ Intercept requests and responses to reverse-engineer APIs, scrape
+ data, or debug network issues. Store captured data in{" "}
+ state and analyze across calls.
+
+
+ {dedent`
+ # Start intercepting
+ playwriter -s 1 -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 -s 1 -e "await page.click('button.load-more')"
+ playwriter -s 1 -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 -s 1 -e "const data = await page.evaluate(async (url) => { const res = await fetch(url); return res.json(); }, state.responses[0].url); console.log(data)"
+ `}
+
+
+ This is faster than scrolling through DOM. Capture the real API
+ calls, inspect their schemas, and replay them with different
+ parameters. Works for pagination, authenticated endpoints, and
+ anything behind JavaScript rendering.
+
+
+
+
+
+
+
+ Record the active tab as video using{" "}
+ chrome.tabCapture. The recording runs in
+ the extension context, so it survives page navigation. Video is saved
+ as MP4.
+
+
+ {dedent`
+ # Start recording
+ playwriter -s 1 -e "await startRecording({ page, outputPath: './recording.mp4', frameRate: 30 })"
+
+ # Navigate, interact — recording continues
+ playwriter -s 1 -e "await page.click('a'); await page.waitForLoadState('domcontentloaded')"
+ playwriter -s 1 -e "await page.goBack()"
+
+ # Stop and save
+ playwriter -s 1 -e "const { path, duration, size } = await stopRecording({ page }); console.log(path, duration + 'ms', size + ' bytes')"
+ `}
+
+
+ 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. 30–60fps. Survives navigation.
+
+
+
+
+
+
+
+ How Playwriter compares to other browser automation approaches.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Control Chrome on any machine from anywhere over the internet.
+ The relay runs on the host alongside Chrome.
+ A{" "}
+ traforo{" "}
+ tunnel exposes it through Cloudflare, giving you a secure public URL.
+ No VPN, no firewall rules, no port forwarding.
+
+
+ {dedent`
+ # On the host machine — start relay with tunnel
+ npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token
+
+ # From anywhere — set env vars and use normally
+ export PLAYWRITER_HOST=https://my-machine-tunnel.traforo.dev
+ export PLAYWRITER_TOKEN=
+ playwriter -s 1 -e "await 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.
+
+
+
+
+
+
+
+ Playwriter is local by default. The relay runs on{" "}
+ localhost:19988 and only accepts connections
+ from the extension. There's 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.
+
+
+
+
+
+
+ );
}
diff --git a/website/src/routes/github.tsx b/website/src/routes/github.tsx
new file mode 100644
index 0000000..adddad1
--- /dev/null
+++ b/website/src/routes/github.tsx
@@ -0,0 +1,9 @@
+import { redirect } from 'react-router';
+
+export const loader = () => {
+ throw redirect('https://github.com/remorses/playwriter');
+};
+
+export default function Index() {
+ return null;
+}
diff --git a/website/src/routes/liveline.tsx b/website/src/routes/liveline.tsx
deleted file mode 100644
index 1d13050..0000000
--- a/website/src/routes/liveline.tsx
+++ /dev/null
@@ -1,487 +0,0 @@
-/*
- * Playwriter editorial page — content only.
- * Components imported from website/src/components/markdown.tsx.
- * Styles from liveline.css and liveline-prism.css.
- */
-
-import type { MetaFunction } from "react-router";
-import dedent from "string-dedent";
-import {
- EditorialPage,
- P,
- A,
- Code,
- Caption,
- CodeBlock,
- Section,
- ComparisonTable,
- List,
- OL,
- Li,
-} from "website/src/components/markdown";
-
-export const meta: MetaFunction = () => {
- const title = "Playwriter - Control your Chrome with Playwright API";
- const description =
- "Chrome extension + CLI for browser automation. Full Playwright API on your existing browser. No new windows, no flags, no context bloat.";
- const image = "https://playwriter.dev/og-image.png";
- return [
- { title },
- { name: "description", content: description },
- { property: "og:title", content: title },
- { property: "og:description", content: description },
- { property: "og:image", content: image },
- { property: "og:image:width", content: "1200" },
- { property: "og:image:height", content: "630" },
- { property: "og:type", content: "website" },
- { property: "og:url", content: "https://playwriter.dev/liveline" },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:title", content: title },
- { name: "twitter:description", content: description },
- { name: "twitter:image", content: image },
- ];
-};
-
-const tocItems = [
- { label: "Getting started", href: "#getting-started" },
- { label: "How it works", href: "#how-it-works" },
- { label: "Snapshots", href: "#snapshots" },
- { label: "Visual labels", href: "#visual-labels" },
- { label: "Sessions", href: "#sessions" },
- { label: "Debugger & editor", href: "#debugger-and-editor" },
- { label: "Network interception", href: "#network-interception" },
- { label: "Screen recording", href: "#screen-recording" },
- { label: "Comparison", href: "#comparison" },
- { label: "Remote access", href: "#remote-access" },
- { label: "Security", href: "#security" },
-];
-
-export default function LivelinePage() {
- return (
-
-
-
- Playwriter lets you control your Chrome browser with the full
- Playwright API. A Chrome extension, a local relay, and a CLI. No new
- browser windows, no Chrome flags, no context bloat.{" "}
- Star on GitHub.
-
-
-
-

-
-
- Your existing Chrome session. Extensions, logins, cookies — all there.
-
-
-
- Every browser automation MCP I tried either spawns a new Chrome
- instance or forces you into a limited set of predefined tools. Playwriter
- does neither. It connects to the browser you already have open,
- exposes the full Playwright API through a single{" "}
- execute tool, and gets out of the way.
- One tool. Any Playwright code. No wrappers.
-
-
-
-
-
- Three steps. Extension, icon click, then you're automating.
-
-
-
- -
- Install the{" "}
- Chrome extension
-
- - Click the extension icon on a tab — it turns green
- - Install CLI and run your first command:
-
-
- {dedent`
- npm i -g playwriter
- playwriter -s 1 -e "await page.goto('https://example.com')"
- `}
-
-
- 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.
-
-
- {dedent`
- playwriter session new # new sandbox, outputs id (e.g. 1)
- playwriter -s 1 -e "await page.goto('https://example.com')"
- playwriter -s 1 -e "console.log(await snapshot({ page }))"
- playwriter -s 1 -e "await page.locator('aria-ref=e5').click()"
- `}
-
-
- Extension icon green = connected. Gray = not attached to this tab.
-
-
-
-
-
-
-
- The extension uses chrome.debugger to
- attach to tabs where you clicked the icon. It opens a WebSocket
- connection to a local relay server. The CLI (or MCP, or your own
- Playwright script) connects to the same relay. CDP commands flow
- through; the extension forwards them to Chrome and sends responses
- back.
-
-
- {dedent`
- ┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
- │ 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) └─────────────────┘
- `}
-
-
- No Chrome restart required. No --remote-debugging-port{" "}
- flags. The extension handles the CDP attachment transparently, and
- the relay multiplexes sessions so multiple agents or CLI instances
- can work with the same browser simultaneously.
-
-
-
-
-
-
-
- The core feedback loop is observe → act → observe.
- Accessibility snapshots are the primary way to read page state. They return
- the full interactive element tree as text, with Playwright locators attached
- to every element.
-
-
- {dedent`
- playwriter -s 1 -e "await 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.
-
-
- {dedent`
- # Search for specific elements
- playwriter -s 1 -e "await snapshot({ page, search: /button|submit/i })"
-
- # Always print URL first, then snapshot — pages can redirect
- playwriter -s 1 -e "console.log('URL:', page.url()); await snapshot({ page }).then(console.log)"
- `}
-
-
- Snapshots are text. They cost a fraction of what screenshots cost in
- tokens. Use them as your primary debugging tool. Only reach for
- screenshots when spatial layout matters — grids, dashboards, maps.
-
-
-
- Accessibility tree as text. 5–20KB vs 100KB+ for screenshots.
-
-
-
-
-
-
-
- For pages where spatial layout matters,{" "}
- screenshotWithAccessibilityLabels overlays{" "}
- Vimium-style labels on every interactive element. Take a screenshot,
- read the labels, click by reference.
-
-
- {dedent`
- playwriter -s 1 -e "await screenshotWithAccessibilityLabels({ page })"
- # Returns screenshot + accessibility snapshot with aria-ref selectors
-
- playwriter -s 1 -e "await 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.
-
-
-
-
-
-
-
- Each session runs in an isolated sandbox with its own{" "}
- state object. Variables, pages, listeners
- persist between calls within a session. Different sessions get
- different state. Browser tabs are shared.
-
-
- {dedent`
- 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 = await 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.
-
-
- {dedent`
- playwriter -s 1 -e "state.myPage = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage(); await state.myPage.goto('https://example.com')"
-
- # All subsequent calls use state.myPage
- playwriter -s 1 -e "console.log(await state.myPage.title())"
- `}
-
-
-
-
-
-
- Full Chrome DevTools Protocol access. Set breakpoints, step through
- code, inspect variables at runtime. Live-edit page scripts and CSS
- without reloading.
-
-
- {dedent`
- # Set breakpoints and debug
- playwriter -s 1 -e "state.cdp = await getCDPSession({ page }); state.dbg = createDebugger({ cdp: state.cdp }); await state.dbg.enable()"
- playwriter -s 1 -e "state.scripts = await state.dbg.listScripts({ search: 'app' }); console.log(state.scripts.map(s => s.url))"
- playwriter -s 1 -e "await state.dbg.setBreakpoint({ file: state.scripts[0].url, line: 42 })"
-
- # Live edit page code
- playwriter -s 1 -e "state.editor = createEditor({ cdp: state.cdp }); await state.editor.enable()"
- playwriter -s 1 -e "await 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.
-
-
-
-
-
-
-
- Intercept requests and responses to reverse-engineer APIs, scrape
- data, or debug network issues. Store captured data in{" "}
- state and analyze across calls.
-
-
- {dedent`
- # Start intercepting
- playwriter -s 1 -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 -s 1 -e "await page.click('button.load-more')"
- playwriter -s 1 -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 -s 1 -e "const data = await page.evaluate(async (url) => { const res = await fetch(url); return res.json(); }, state.responses[0].url); console.log(data)"
- `}
-
-
- This is faster than scrolling through DOM. Capture the real API
- calls, inspect their schemas, and replay them with different
- parameters. Works for pagination, authenticated endpoints, and
- anything behind JavaScript rendering.
-
-
-
-
-
-
-
- Record the active tab as video using{" "}
- chrome.tabCapture. The recording runs in
- the extension context, so it survives page navigation. Video is saved
- as MP4.
-
-
- {dedent`
- # Start recording
- playwriter -s 1 -e "await startRecording({ page, outputPath: './recording.mp4', frameRate: 30 })"
-
- # Navigate, interact — recording continues
- playwriter -s 1 -e "await page.click('a'); await page.waitForLoadState('domcontentloaded')"
- playwriter -s 1 -e "await page.goBack()"
-
- # Stop and save
- playwriter -s 1 -e "const { path, duration, size } = await stopRecording({ page }); console.log(path, duration + 'ms', size + ' bytes')"
- `}
-
-
- 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. 30–60fps. Survives navigation.
-
-
-
-
-
-
-
- How Playwriter compares to other browser automation approaches.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Control Chrome on any machine from anywhere over the internet.
- The relay runs on the host alongside Chrome.
- A{" "}
- traforo{" "}
- tunnel exposes it through Cloudflare, giving you a secure public URL.
- No VPN, no firewall rules, no port forwarding.
-
-
- {dedent`
- # On the host machine — start relay with tunnel
- npx -y traforo -p 19988 -t my-machine -- npx -y playwriter serve --token
-
- # From anywhere — set env vars and use normally
- export PLAYWRITER_HOST=https://my-machine-tunnel.traforo.dev
- export PLAYWRITER_TOKEN=
- playwriter -s 1 -e "await 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.
-
-
-
-
-
-
-
- Playwriter is local by default. The relay runs on{" "}
- localhost:19988 and only accepts connections
- from the extension. There's 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.
-
-
-
-
-
-
- );
-}