Playwright's API on a Rust CDP engine — Chromium browser automation for Python & Node, no driver subprocess. (Alpha)
884
stars
151
commits
Python
primary language
Sep 11, 2026
updated
A Rust rewrite of Playwright, a popular browser automation library. Rustwright is interoperable with Playwright but runs on an in-process Rust CDP engine — 2.55× faster and 70% less memory (no Node driver), with no Playwright automation fingerprint. Alpha; Chromium-only.
CPython 3.15 support is verified on 3.15.0rc1 and remains pre-release; CI tracks 3.15-dev until GA.
Rustwright is a browser automation library for Python and Node.js that keeps the Playwright API you already know but drives Chromium from a native Rust engine speaking raw Chrome DevTools Protocol — no driver subprocess in the path.
playwright-python: your code ──pipe──► Node driver ──CDP──► Chromium
rustwright: your code ────────── raw CDP ──────────► Chromium
Rustwright is interoperable with Playwright — install it, change one import, and your existing code runs on the Rust engine.
Python
pip install rustwright
python -m rustwright install chromium
- from playwright.sync_api import sync_playwright
+ from rustwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
browser.close()
Node.js (experimental)
Install from npm:
npm install rustwright
The Node binding drives an existing Chromium/Chrome — point Rustwright at it with RUSTWRIGHT_CHROMIUM, CHROME, or CHROMIUM. Prefer to build from source? git clone the repo and run npm install && npm run build in node/.
- import { chromium } from 'playwright';
+ import { chromium } from 'rustwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();
See the network-independent examples/quickstart.js example. After building the Node binding from source, run it from the repository root with node examples/quickstart.js.
Only a subset of the API surface is bridged — see Limitations.
CLI (agent-focused)
Install the native rustwright-cli with one command:
curl -fsSL https://raw.githubusercontent.com/Skyvern-AI/rustwright/main/install.sh | sh
Then drive a persistent Chromium session from your shell or an agent loop:
rustwright-cli open https://example.com
rustwright-cli snapshot # compact page tree with @eN refs
rustwright-cli click @e1
rustwright-cli close
See cli/README.md for the full command surface. For a Model Context Protocol
server, use the standalone rustwright-mcp package.
playwright-python launches and pipes to a bundled Node driver. Rustwright's engine is native — the browser-control code runs in-process.Input.dispatchMouseEvent), not synthetic element.click() DOM calls. Untrusted DOM shortcuts are opt-in only.frame_locator() across origins.One Rust core — an async CDP client built on Tokio (WebSocket, with opt-in Unix-pipe transport) — talks to Chromium directly, and thin PyO3 (Python) and napi-rs (Node) bindings expose it in-process. The two-line diagram above is the entire architecture.
Already have a Chromium/Chrome binary? Point Rustwright at it with RUSTWRIGHT_CHROMIUM, CHROME, or CHROMIUM.
Give an agent or shell script a browser through compact accessibility snapshots with element refs (e1, e2, …), instead of raw HTML or screenshots. Refs are session-scoped, never reused, and best-effort rather than a security boundary; snapshots include page values but mask password fields.
rustwright-mcp is a native Rust MCP server (no Python or Node runtime in the
serving path) that gives any MCP client browser_* tools over stdio, with
compact accessibility snapshots and inline PNG screenshots. It lives in
mcp/ and is the canonical Rustwright MCP server.
From source (needs a Rust toolchain):
cargo install --git https://github.com/Skyvern-AI/rustwright rustwright-mcp
This installs the server binary as rustwright-mcp. An npm package (rustwright-mcp,
prebuilt binaries via npx rustwright-mcp) is on the way.
If Chrome or Chromium is already installed, point Rustwright at it with
RUSTWRIGHT_CHROMIUM (or CHROME / CHROMIUM). Otherwise download one with
pip install rustwright && python -m rustwright install chromium.
claude mcp add rustwright -- rustwright-mcp
Open ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows, then use:
{
"mcpServers": {
"rustwright": {
"command": "rustwright-mcp"
}
}
}
{
"mcpServers": {
"rustwright": {
"command": "rustwright-mcp"
}
}
}
| If... | Do this |
|---|---|
| You already have Chrome/Chromium | Set RUSTWRIGHT_CHROMIUM (or CHROME / CHROMIUM) to its executable in the server's env. |
| You do not have a browser installed | pip install rustwright && python -m rustwright install chromium — the server finds the downloaded browser automatically. |
| Screenshots are too large to inline | Tune RUSTWRIGHT_MCP_SCREENSHOT_MAX_BYTES; oversized captures fall back to a temp-file path instead of inline image content. |
See mcp/README.md for the full tool list and
configuration. Setting up via an AI agent? Tell it to fetch
https://raw.githubusercontent.com/Skyvern-AI/rustwright/HEAD/mcp/README.md
and follow it.
The earlier Python MCP server is deprecated: it stays in place until the native server reaches full tool parity, but new capabilities land in
mcp/only.
Drive one persistent Chromium session straight from the rustwright command, with no application code.
pip install rustwright
python -m rustwright install chromium
rustwright open example.com
rustwright snapshot
rustwright click e1001
rustwright close
snapshot shows refs; act by ref, then use the fresh snapshot returned after the action.--json for one JSON object per command when scripting.--session NAME for named browser sessions.See the agent interface guide for every verb, flag, and security detail.
Rustwright drives browsers — but you still need somewhere to run them. Skyvern (the team behind Rustwright) offers hosted Browser Sessions as a paid service that funds this project.
Features:
Each session returns a browser_address CDP endpoint that Rustwright connects to like any remote Chromium (sessions bill while open).
Use chromium.connect_over_cdp() for every remote Chromium connection.
BrowserType.connect() is not a CDP alias and does not support Playwright
run-server endpoints. See the remote-browser guide
for migration steps, endpoint diagnostics, security guidance, and the pinned
Open WebUI compose recipe.
Get started:
pip install skyvernimport asyncio
from rustwright.async_api import async_playwright
from skyvern import Skyvern
async def main():
session = await Skyvern(api_key="<SKYVERN_API_KEY>").create_browser_session()
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(session.browser_address)
page = await browser.new_page()
await page.goto("https://example.com")
asyncio.run(main())
Remote sessions are Python-only for now — Rustwright's Node binding doesn't support
connect_over_cdpyet (it's on the Roadmap).
Because Rustwright never loads Playwright's Node driver, it never emits the automation signatures that ship with it:
__playwright__binding__ / utility-world globals, no driver bootstrap. The backend reports playwright_driver: "none".Runtime.enable on the default path — a normal launch + navigate never enables the CDP Runtime domain, closing the Runtime.enable console-serialization leak behind isAutomatedWithCDP. (Console/page-error/binding opt-ins still enable it lazily — detectable by design.)--disable-blink-features=AutomationControlled, rewrites HeadlessChrome/ → Chrome/ in the UA and client hints, and installs a navigator.webdriver cleanup init script.Local fingerprint runs — default Playwright failed webdriver/headless checks that Rustwright passed; these are local diagnostics, not a guarantee:
| Probe | Result |
|---|---|
| SannySoft | ✅ Clean |
| BrowserScan | ✅ Clean |
| DeviceAndBrowserInfo | ✅ Clean (after the Runtime-domain cleanup) |
| CreepJS | ⚠️ Detects headless |
[!IMPORTANT] Rustwright is not "undetectable." It is not a CAPTCHA or Cloudflare bypass, and it is not fully CDP-invisible — it still uses CDP primitives (
Target.setAutoAttach, init scripts, and lazyRuntime.enablefor console event/pageerror event/binding opt-ins). The claim is narrow: no Playwright-specific automation fingerprint, plus baseline signal hygiene.
The headline numbers are local diagnostics, not yet capped-CI evidence. On speed, one dev-host run (warm browser, 5 iterations) won 16 of 17 case means:
| Run | Cases | Rustwright | playwright-python | Speedup |
|---|---|---|---|---|
| Local dev host (warm browser, 5 iterations) | 17 | 5,256 ms | 13,418 ms | 2.55× |
Treat it as a diagnostic, not a launch claim — it is not capped-Docker/CI evidence. Methodology: BENCHMARK.md.
On memory, a form-fill diagnostic recorded the client library's footprint at 133.5 MiB for playwright-python (Python + Node driver) versus 40.6 MiB for Rustwright (no driver) — about 70% less; a separate async-concurrency diagnostic measured ~66% less on the same client-stack basis. Both cover the part the library controls — Chromium-dominated whole-process memory is roughly equal — and both are demo-grade diagnostics, not capped-CI evidence.
| Rustwright | playwright-python | Puppeteer | Patchright | |
|---|---|---|---|---|
| API | Playwright-shaped (Py + Node) | Official Python Playwright | JS/TS Puppeteer | Playwright drop-in fork |
| Engine / transport | Rust core, raw CDP | Python → Node driver | Node over CDP | Patched PW driver |
| In-process engine (no driver subprocess) | ✅ | ❌ bundled Node driver | ✅ Node is the runtime | ❌ Playwright-style driver |
| Browsers | Chromium only | Chromium, Firefox, WebKit | Chrome, Firefox | Chromium-based |
| Default input | Trusted CDP events | Browser-level | Browser / CDP | Playwright + stealth |
| Cross-origin iframes | OOPIF (alpha) | Mature | Frame APIs | Inherits Playwright |
| Playwright fingerprint | No | Yes | n/a | Patched |
| Maturity | 🟠 Alpha | 🟢 Mature | 🟢 Mature | 🟡 Focused fork |
Rustwright's lane: a Rust CDP engine under the Playwright API, for Chromium.
See LIMITATIONS.md for detail.
docs/PARITY.md.launch, newPage, goto, click, fill, title, textContent, evaluate, screenshot, close); contexts, routing, tracing, and locators are Python-only for now.JSHandle follow-ups and drag/screenshot/bounding-box.Recently shipped:
bindings/CONTRACT.md); cross-language equivalence gated in CIRuntime.enable console-serialization leak closed on the default pathFirefox and WebKit are not planned — Rustwright is deliberately Chromium-only.
Rustwright is Rust + Python + Node. cargo builds the engine; maturin develop --release installs the Python package; cd node && npm run build builds the Node addon; the Python suite exercises the engine against real Chromium. Full Docker gate: 1,046 tests pass (6 skipped), plus 515/515 shared parity cases run against real Playwright; CI (test.yml) runs a fast representative subset on every PR.
See CONTRIBUTING.md for build details and the code-layout reality.
Rustwright is an early alpha from Skyvern, developed in the open. If the architecture resonates, give it a ⭐.
Questions, ideas, or want to help? Join the Skyvern community on Discord.
Rustwright sends one engine_launched event per process to PostHog with the Rustwright version, operating system, and CPU architecture. It sends no URLs or page content.
Events carry a random installation ID, generated without using the hostname, username, or MAC address, stored at ~/.cache/rustwright/telemetry_id (or the $XDG_CACHE_HOME/%LOCALAPPDATA% equivalent) so events from the same installation can be counted together. Delete the file to reset it. Events are marked personless, and GeoIP enrichment is disabled at the event level. The request, like any HTTPS request, exposes the sender's IP address to PostHog, but Rustwright does not store the IP address in the event payload.
To opt out:
export DISABLE_TELEMETRY=1 # or DO_NOT_TRACK=1
MIT © 2026 Ikonomos Inc (dba Skyvern)
Python
60.8%
Rust
34.0%
Playwright's API on a Rust CDP engine — Chromium browser automation for Python & Node, no driver subprocess. (Alpha)
884
stars
151
commits
Python
primary language
Sep 11, 2026
updated
A Rust rewrite of Playwright, a popular browser automation library. Rustwright is interoperable with Playwright but runs on an in-process Rust CDP engine — 2.55× faster and 70% less memory (no Node driver), with no Playwright automation fingerprint. Alpha; Chromium-only.
CPython 3.15 support is verified on 3.15.0rc1 and remains pre-release; CI tracks 3.15-dev until GA.
Rustwright is a browser automation library for Python and Node.js that keeps the Playwright API you already know but drives Chromium from a native Rust engine speaking raw Chrome DevTools Protocol — no driver subprocess in the path.
playwright-python: your code ──pipe──► Node driver ──CDP──► Chromium
rustwright: your code ────────── raw CDP ──────────► Chromium
Rustwright is interoperable with Playwright — install it, change one import, and your existing code runs on the Rust engine.
Python
pip install rustwright
python -m rustwright install chromium
- from playwright.sync_api import sync_playwright
+ from rustwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
browser.close()
Node.js (experimental)
Install from npm:
npm install rustwright
The Node binding drives an existing Chromium/Chrome — point Rustwright at it with RUSTWRIGHT_CHROMIUM, CHROME, or CHROMIUM. Prefer to build from source? git clone the repo and run npm install && npm run build in node/.
- import { chromium } from 'playwright';
+ import { chromium } from 'rustwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();
See the network-independent examples/quickstart.js example. After building the Node binding from source, run it from the repository root with node examples/quickstart.js.
Only a subset of the API surface is bridged — see Limitations.
CLI (agent-focused)
Install the native rustwright-cli with one command:
curl -fsSL https://raw.githubusercontent.com/Skyvern-AI/rustwright/main/install.sh | sh
Then drive a persistent Chromium session from your shell or an agent loop:
rustwright-cli open https://example.com
rustwright-cli snapshot # compact page tree with @eN refs
rustwright-cli click @e1
rustwright-cli close
See cli/README.md for the full command surface. For a Model Context Protocol
server, use the standalone rustwright-mcp package.
playwright-python launches and pipes to a bundled Node driver. Rustwright's engine is native — the browser-control code runs in-process.Input.dispatchMouseEvent), not synthetic element.click() DOM calls. Untrusted DOM shortcuts are opt-in only.frame_locator() across origins.One Rust core — an async CDP client built on Tokio (WebSocket, with opt-in Unix-pipe transport) — talks to Chromium directly, and thin PyO3 (Python) and napi-rs (Node) bindings expose it in-process. The two-line diagram above is the entire architecture.
Already have a Chromium/Chrome binary? Point Rustwright at it with RUSTWRIGHT_CHROMIUM, CHROME, or CHROMIUM.
Give an agent or shell script a browser through compact accessibility snapshots with element refs (e1, e2, …), instead of raw HTML or screenshots. Refs are session-scoped, never reused, and best-effort rather than a security boundary; snapshots include page values but mask password fields.
rustwright-mcp is a native Rust MCP server (no Python or Node runtime in the
serving path) that gives any MCP client browser_* tools over stdio, with
compact accessibility snapshots and inline PNG screenshots. It lives in
mcp/ and is the canonical Rustwright MCP server.
From source (needs a Rust toolchain):
cargo install --git https://github.com/Skyvern-AI/rustwright rustwright-mcp
This installs the server binary as rustwright-mcp. An npm package (rustwright-mcp,
prebuilt binaries via npx rustwright-mcp) is on the way.
If Chrome or Chromium is already installed, point Rustwright at it with
RUSTWRIGHT_CHROMIUM (or CHROME / CHROMIUM). Otherwise download one with
pip install rustwright && python -m rustwright install chromium.
claude mcp add rustwright -- rustwright-mcp
Open ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows, then use:
{
"mcpServers": {
"rustwright": {
"command": "rustwright-mcp"
}
}
}
{
"mcpServers": {
"rustwright": {
"command": "rustwright-mcp"
}
}
}
| If... | Do this |
|---|---|
| You already have Chrome/Chromium | Set RUSTWRIGHT_CHROMIUM (or CHROME / CHROMIUM) to its executable in the server's env. |
| You do not have a browser installed | pip install rustwright && python -m rustwright install chromium — the server finds the downloaded browser automatically. |
| Screenshots are too large to inline | Tune RUSTWRIGHT_MCP_SCREENSHOT_MAX_BYTES; oversized captures fall back to a temp-file path instead of inline image content. |
See mcp/README.md for the full tool list and
configuration. Setting up via an AI agent? Tell it to fetch
https://raw.githubusercontent.com/Skyvern-AI/rustwright/HEAD/mcp/README.md
and follow it.
The earlier Python MCP server is deprecated: it stays in place until the native server reaches full tool parity, but new capabilities land in
mcp/only.
Drive one persistent Chromium session straight from the rustwright command, with no application code.
pip install rustwright
python -m rustwright install chromium
rustwright open example.com
rustwright snapshot
rustwright click e1001
rustwright close
snapshot shows refs; act by ref, then use the fresh snapshot returned after the action.--json for one JSON object per command when scripting.--session NAME for named browser sessions.See the agent interface guide for every verb, flag, and security detail.
Rustwright drives browsers — but you still need somewhere to run them. Skyvern (the team behind Rustwright) offers hosted Browser Sessions as a paid service that funds this project.
Features:
Each session returns a browser_address CDP endpoint that Rustwright connects to like any remote Chromium (sessions bill while open).
Use chromium.connect_over_cdp() for every remote Chromium connection.
BrowserType.connect() is not a CDP alias and does not support Playwright
run-server endpoints. See the remote-browser guide
for migration steps, endpoint diagnostics, security guidance, and the pinned
Open WebUI compose recipe.
Get started:
pip install skyvernimport asyncio
from rustwright.async_api import async_playwright
from skyvern import Skyvern
async def main():
session = await Skyvern(api_key="<SKYVERN_API_KEY>").create_browser_session()
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(session.browser_address)
page = await browser.new_page()
await page.goto("https://example.com")
asyncio.run(main())
Remote sessions are Python-only for now — Rustwright's Node binding doesn't support
connect_over_cdpyet (it's on the Roadmap).
Because Rustwright never loads Playwright's Node driver, it never emits the automation signatures that ship with it:
__playwright__binding__ / utility-world globals, no driver bootstrap. The backend reports playwright_driver: "none".Runtime.enable on the default path — a normal launch + navigate never enables the CDP Runtime domain, closing the Runtime.enable console-serialization leak behind isAutomatedWithCDP. (Console/page-error/binding opt-ins still enable it lazily — detectable by design.)--disable-blink-features=AutomationControlled, rewrites HeadlessChrome/ → Chrome/ in the UA and client hints, and installs a navigator.webdriver cleanup init script.Local fingerprint runs — default Playwright failed webdriver/headless checks that Rustwright passed; these are local diagnostics, not a guarantee:
| Probe | Result |
|---|---|
| SannySoft | ✅ Clean |
| BrowserScan | ✅ Clean |
| DeviceAndBrowserInfo | ✅ Clean (after the Runtime-domain cleanup) |
| CreepJS | ⚠️ Detects headless |
[!IMPORTANT] Rustwright is not "undetectable." It is not a CAPTCHA or Cloudflare bypass, and it is not fully CDP-invisible — it still uses CDP primitives (
Target.setAutoAttach, init scripts, and lazyRuntime.enablefor console event/pageerror event/binding opt-ins). The claim is narrow: no Playwright-specific automation fingerprint, plus baseline signal hygiene.
The headline numbers are local diagnostics, not yet capped-CI evidence. On speed, one dev-host run (warm browser, 5 iterations) won 16 of 17 case means:
| Run | Cases | Rustwright | playwright-python | Speedup |
|---|---|---|---|---|
| Local dev host (warm browser, 5 iterations) | 17 | 5,256 ms | 13,418 ms | 2.55× |
Treat it as a diagnostic, not a launch claim — it is not capped-Docker/CI evidence. Methodology: BENCHMARK.md.
On memory, a form-fill diagnostic recorded the client library's footprint at 133.5 MiB for playwright-python (Python + Node driver) versus 40.6 MiB for Rustwright (no driver) — about 70% less; a separate async-concurrency diagnostic measured ~66% less on the same client-stack basis. Both cover the part the library controls — Chromium-dominated whole-process memory is roughly equal — and both are demo-grade diagnostics, not capped-CI evidence.
| Rustwright | playwright-python | Puppeteer | Patchright | |
|---|---|---|---|---|
| API | Playwright-shaped (Py + Node) | Official Python Playwright | JS/TS Puppeteer | Playwright drop-in fork |
| Engine / transport | Rust core, raw CDP | Python → Node driver | Node over CDP | Patched PW driver |
| In-process engine (no driver subprocess) | ✅ | ❌ bundled Node driver | ✅ Node is the runtime | ❌ Playwright-style driver |
| Browsers | Chromium only | Chromium, Firefox, WebKit | Chrome, Firefox | Chromium-based |
| Default input | Trusted CDP events | Browser-level | Browser / CDP | Playwright + stealth |
| Cross-origin iframes | OOPIF (alpha) | Mature | Frame APIs | Inherits Playwright |
| Playwright fingerprint | No | Yes | n/a | Patched |
| Maturity | 🟠 Alpha | 🟢 Mature | 🟢 Mature | 🟡 Focused fork |
Rustwright's lane: a Rust CDP engine under the Playwright API, for Chromium.
See LIMITATIONS.md for detail.
docs/PARITY.md.launch, newPage, goto, click, fill, title, textContent, evaluate, screenshot, close); contexts, routing, tracing, and locators are Python-only for now.JSHandle follow-ups and drag/screenshot/bounding-box.Recently shipped:
bindings/CONTRACT.md); cross-language equivalence gated in CIRuntime.enable console-serialization leak closed on the default pathFirefox and WebKit are not planned — Rustwright is deliberately Chromium-only.
Rustwright is Rust + Python + Node. cargo builds the engine; maturin develop --release installs the Python package; cd node && npm run build builds the Node addon; the Python suite exercises the engine against real Chromium. Full Docker gate: 1,046 tests pass (6 skipped), plus 515/515 shared parity cases run against real Playwright; CI (test.yml) runs a fast representative subset on every PR.
See CONTRIBUTING.md for build details and the code-layout reality.
Rustwright is an early alpha from Skyvern, developed in the open. If the architecture resonates, give it a ⭐.
Questions, ideas, or want to help? Join the Skyvern community on Discord.
Rustwright sends one engine_launched event per process to PostHog with the Rustwright version, operating system, and CPU architecture. It sends no URLs or page content.
Events carry a random installation ID, generated without using the hostname, username, or MAC address, stored at ~/.cache/rustwright/telemetry_id (or the $XDG_CACHE_HOME/%LOCALAPPDATA% equivalent) so events from the same installation can be counted together. Delete the file to reset it. Events are marked personless, and GeoIP enrichment is disabled at the event level. The request, like any HTTPS request, exposes the sender's IP address to PostHog, but Rustwright does not store the IP address in the event payload.
To opt out:
export DISABLE_TELEMETRY=1 # or DO_NOT_TRACK=1
MIT © 2026 Ikonomos Inc (dba Skyvern)
Python
60.8%
Rust
34.0%