The Switches That Give Away the Driver: Inside Patchright, the Undetected Playwright
Playwright is arguably the most capable browser automation library ever shipped, and that is precisely its problem: it is also the best-known. Anti-bot vendors run it daily against their own detectors, so a stock Playwright session announces itself through a dozen channels before your first page.goto() renders anything. This article is a hands-on teardown of Patchright - a patched Playwright driver, not a browser fork - covering exactly which signals vanilla Playwright leaks (measured, not folklore), how Patchright’s driver patches neutralize them, what those patches cost you, and where the remaining holes are. All outputs are real runs from October 3, 2026: Playwright 1.63.0, Patchright 1.63.0, Chromium 153.0.8010.12.
Introduction
In our article on coherent identity spoofing with Camoufox, we took the Firefox branch of the anti-detection tree: a Gecko fork that intercepts fingerprint values inside the engine, wrapped in a statistical identity generator. We closed with a caveat that deserves its own follow-up: when a target’s protection probes JavaScript engine internals, a Chromium-based approach is the better fit, and the tool we pointed at was Patchright. This article is that follow-up: the Chromium branch, and specifically the question of what actually separates a “stealth” Playwright setup from a detected one.
The framing matters, because the scraping ecosystem is full of cargo cult on this topic. Stack Overflow answers from 2019 still recommend setting navigator.webdriver = undefined from page.evaluate and calling it a day. Modern detection does not work that way. As we covered in the Camoufox article, property patching from JavaScript leaves fingerprints of its own, and the signals that matter now live at a lower layer than the page: in the command line the driver hands the browser, and in the DevTools protocol traffic between the driver and the renderer. Those two surfaces are invisible to page.evaluate-style fixes, and they are what Patchright actually attacks.
Patchright is not new, but it is unusually well engineered for this space, and it is in active, verifiable motion: the Python package sits at 1.63.0, mirroring Playwright 1.63.0, with the driver repo’s v1.63.0 release published September 8, 2026 and fixes landing as recently as October 1. It is maintained as a set of code patches applied on top of the stock Playwright driver, automatically redeployed on every upstream release, and it ships as a drop-in replacement: change one import, keep the rest of your code.
We will put measurable numbers on all of it. Every claim in this article comes from one of two places: the project’s own patch files and documentation (primary sources, quoted as such), or runs we executed ourselves on a single machine with pinned versions. Where the two disagree, we say so.
How Sites See Your Driver
Before the patches, the leaks. When a Playwright session connects to a site, the detectable surface splits into three families.
Family 1: The switch list
Chromium documents hundreds of command line switches, and automation drivers pass a long, very stable list of them. These switches are not directly readable from JavaScript, but several of them have observable side effects that are:
--enable-automationand the Blink feature it enables setnavigator.webdrivertotrue- the flag whose entire documented purpose is to announce automation (MDN).- Headless configurations without a GPU fall back to software GL, and the WebGL renderer string says
SwiftShaderout loud. - The headless shell binary - the stripped-down Chromium build Playwright launches by default in headless mode - lacks
window.chrome, reports zero plugins, mismatches the Permissions API againstNotification.permission, and puts the literal tokenHeadlessChromein the user agent.
None of this is exotic. The bot.sannysoft.com test page has checked most of it for years.
Family 2: The Runtime.enable leak (the big one)
This is the one that killed the naive era, and it deserves a careful explanation because it is still poorly understood.
All major automation libraries - Playwright, Puppeteer, Selenium with ChromeDriver - talk to the browser through the Chrome DevTools Protocol (CDP). To run JavaScript in a page, the driver needs to know the execution context IDs of every frame, and the standard way to learn them is the CDP command Runtime.enable. Stock Playwright calls it for every frame, from the moment the page loads.
The side effect: once the Runtime domain is enabled, every console.* call in the page emits a Runtime.consoleAPICalled event to the driver, and generating that event serializes the call’s arguments. In June 2024, bot researcher Antoine Vastel (then DataDome’s VP of Research) published the detection consequence, which fits in four lines:
let cdp = false;
Error.prepareStackTrace = () => { cdp = true; return ""; };
console.debug(new Error());
// cdp === true -> a CDP Runtime client is listening
Passing an Error to console.debug is a no-op for a normal browser. But the event serialization formats the error’s stack, which routes through Error.prepareStackTrace - a page-visible hook. The getter fires only because a protocol client subscribed. No flag, no UA string, no GPU oddity: a structural property of how the driver communicates. The Brotector test page implements refined variants of this probe - counting accesses to Error.prototype.name as well, with timing checks to distinguish CDP from a human’s open DevTools.
The Rebrowser project documented in 2024 that this technique “is used by all major anti-bot software,” after experimentally disabling Runtime.enable in Puppeteer and Playwright and watching instant CAPTCHAs disappear on protected sites with everything else held constant. Treat that as their reported experiment, but our own numbers below are consistent with it: the probe fires on every vanilla configuration we tested.
Family 3: The artifacts of scale
A third family is everything the driver does around the browser: utility-world names in the JavaScript environment, sourceURL annotations on injected scripts, init-script injection mechanics, and the timing fingerprints of the driver’s bookkeeping. These are less commonly the kill signal but they round out the picture of what a defender can score.
What Patchright Actually Is
The critical design fact: Patchright does not fork the browser. Camoufox patches Firefox at the C++ engine level; Patchright leaves Chromium untouched and instead patches the driver - the Node.js bundle that ships inside the playwright package and speaks CDP to the browser. That choice has three consequences:
- No custom-browser fingerprint. You run a stock Chromium build (or better, real Google Chrome), so you inherit its entire, unremarkable fingerprint surface. A custom-built Chromium is itself a fingerprint outlier, as the Rebrowser writeup notes.
- Upstream tracking. The patches are maintained as TypeScript transforms over the driver source (
driver_patches/in the repository), redeployed automatically when Playwright releases. The Python, Node.js, and community .NET packages follow shortly after - the Python 1.63.0 package landed on PyPI on September 20, twelve days after the driver. - A defined patch surface. You can read exactly what changes. Nothing is smuggled in a compiled binary.
The patch files are the authoritative documentation, and they are worth reading. chromiumSwitchesPatch.ts removes thirteen of Playwright’s default switches and adds exactly one: --disable-blink-features=AutomationControlled, which is what forces navigator.webdriver to report false. chromiumPatch.ts removes --enable-unsafe-swiftshader. The Runtime.enable fix follows the isolated-world strategy the Rebrowser project described: execute automation JavaScript in isolated execution contexts created per frame (Page.createIsolatedWorld) instead of enabling the Runtime domain to discover context IDs. The Console domain is disabled outright, closing the serialization side channel at the cost of console event delivery. Init scripts - which normally depend on Runtime bindings - are injected by intercepting HTML responses through Playwright’s routing, a mechanism the project honestly documents as theoretically detectable by timing, “but no antibot currently checks for this kind of Timing Attack.”
There is one bonus patch that surprises people: Patchright’s selector engine pierces closed shadow roots, which vanilla Playwright cannot see into at all. If you have ever fought a component library that hides its DOM behind attachShadow({mode: 'closed'}), that is a scraping quality-of-life feature, not just a stealth one.
The PoC: Four Configurations, One Machine
The PoC has three pieces: a local detection page (detector_server.py, stdlib only) implementing the eight checks from the leak families above, including the Vastel/Brotector-style CDP probe; a probe script (probe.py) that launches each driver in each mode, dumps the driver’s launch flags, and runs both the local detector and bot.sannysoft.com; and extras_check.py for the two behavioral differences worth knowing about.
Setup is a single venv with both drivers pinned to the same upstream version:
$ python3 -m venv venv && source venv/bin/activate
$ pip install playwright==1.63.0 patchright==1.63.0
$ playwright install chromium && playwright install chromium-headless-shell
$ patchright install chromium
We ran four configurations, all on the same GPU-less Linux VM on October 3, 2026 (Python 3.12):
| # | Driver | Mode | Binary actually launched |
|---|---|---|---|
| 1 | vanilla Playwright | headless (default) | headless shell 153.0.8010.12 |
| 2 | vanilla Playwright | new headless (channel="chromium") |
full Chromium 153.0.8010.12 |
| 3 | Patchright | headless (drop-in, zero code changes) | headless shell 153.0.8010.12 |
| 4 | Patchright | new headless (channel="chromium") |
full Chromium 153.0.8010.12 |
Configuration 1: the tutorial default
What most scraping code in the wild does - p.chromium.launch(headless=True) and go:
$ python probe.py --driver vanilla --mode headless --out out
[*] Local detector results:
LEAK: navigator.webdriver
LEAK: cdp_runtime_probe
LEAK: window.chrome missing
LEAK: navigator.plugins empty
LEAK: permission mismatch
LEAK: software WebGL
LEAK: HeadlessChrome UA
[*] bot.sannysoft.com: 15 passed, 3 failed
failed: WebDriver
failed: Chrome
failed: Plugins is of type PluginArray
Seven leaks. Two of them (webdriver, the CDP probe) are fatal on any serious protection; the rest are the headless shell announcing its lineage. The flags dump shows the other half of the story - this is what the driver hands the browser:
[*] Automation-relevant flags present:
--disable-component-update
--disable-extensions
--disable-popup-blocking
--disable-default-apps
--enable-unsafe-swiftshader
--headless
--no-first-run
--disable-infobars
--disable-sync
--metrics-recording-only
--use-mock-keychain
--disable-features
(How do we see the flags? Chromium sanitizes its /proc command line after startup, and CDP’s Browser.getBrowserCommandLine refuses to answer without --enable-automation. The probe uses a third route: point executable_path at a file that exists but is not executable, and the driver’s launch error prints its full call log, every switch included. The driver confesses when it fails.)

Configuration 2: vanilla, but the real browser
A common “we fixed it” attempt: launch the full Chromium binary instead of the shell (channel="chromium"), which since Chrome 129 gives you the new headless mode - the same code path as headed Chrome:
$ python probe.py --driver vanilla --mode new-headless --out out
[*] Local detector results:
LEAK: navigator.webdriver
LEAK: cdp_runtime_probe
LEAK: software WebGL
LEAK: HeadlessChrome UA
[*] bot.sannysoft.com: 21 passed, 1 failed
failed: WebDriver
Progress: window.chrome exists, plugins populate, the permissions mismatch vanishes. But the two lethal leaks survive untouched, and this is the pedagogical heart of the whole article: the browser binary is not the problem. The driver is. Vastel’s 2024 research made exactly this point - the new headless Chrome is a legitimate browser; the CDP signal comes from the automation library riding on top of it. navigator.webdriver also still reads true here, because nothing disabled the AutomationControlled Blink feature.
Configuration 3: the drop-in swap
Now the one-line change - same script, from patchright.sync_api import sync_playwright instead of the Playwright import, everything else identical:
$ python probe.py --driver patchright --mode headless --out out
[*] Local detector results:
LEAK: window.chrome missing
LEAK: navigator.plugins empty
LEAK: permission mismatch
LEAK: software WebGL
LEAK: HeadlessChrome UA
[*] bot.sannysoft.com: 16 passed, 2 failed
failed: Chrome
failed: Plugins is of type PluginArray
Look at what vanished: navigator.webdriver is gone (the added --disable-blink-features=AutomationControlled doing its one job), and the CDP runtime probe is silent. The isolated-world patch works as documented. The flags dump confirms the other half - of the twelve automation-associated defaults vanilla passes, Patchright drops six and adds exactly one:
[*] Automation-relevant flags present:
--disable-blink-features=AutomationControlled
--headless
--no-first-run
--disable-infobars
--disable-sync
--use-mock-keychain
--disable-features
[*] Absent (not passed by this driver):
--disable-component-update
--disable-extensions
--disable-popup-blocking
--disable-default-apps
--enable-unsafe-swiftshader
--metrics-recording-only
But notice what remains: the headless shell tells. Drop-in Patchright in default headless mode still launches the stripped shell binary, and half the classic tells come right back. The README’s own “best practice” section is explicit about this - being truly undetected requires more than the import swap.
Configuration 4: Patchright driving the full binary
$ python probe.py --driver patchright --mode new-headless --out out
[*] Local detector results:
LEAK: software WebGL
LEAK: HeadlessChrome UA
[*] bot.sannysoft.com: 22 passed, 0 failed
Twenty-two for zero on sannysoft. The two residual leaks are environmental, not driver leaks: our VM has no GPU, so every headless configuration here falls back to SwiftShader software rendering (Patchright at least removes the --enable-unsafe-swiftshader flag that vanilla passes), and the new-headless UA carries the HeadlessChrome token by design. Both vanish under the project’s recommended production setup: the real Google Chrome binary (channel="chrome"), headful, in a persistent context. We could not run headful in our display-less test VM - which is itself worth stating, because that constraint is one reason scrapers reach for headless in the first place, and with Patchright it is a trade-off you are consciously making, not a default you forgot about.



The full matrix
| Check | 1: vanilla shell | 2: vanilla full | 3: Patchright shell | 4: Patchright full |
|---|---|---|---|---|
navigator.webdriver |
true | true | false | false |
| CDP runtime probe | fires | fires | silent | silent |
window.chrome |
missing | ok | missing | ok |
navigator.plugins |
0 | 5 | 0 | 5 |
| Permissions vs Notification | mismatch | ok | mismatch | ok |
| WebGL renderer | SwiftShader | SwiftShader | SwiftShader | SwiftShader |
| UA token | HeadlessChrome | HeadlessChrome | HeadlessChrome | HeadlessChrome |
| bot.sannysoft.com | 15/3 | 21/1 | 16/2 | 22/0 |
Two behavioral differences worth knowing
extras_check.py measures the two side effects you will actually feel in daily use:
$ python extras_check.py --driver vanilla
[*] closed shadow root : cannot see inside (TimeoutError)
[*] console events : received ['hello-from-page']
$ python extras_check.py --driver patchright
[*] closed shadow root : pierced (found 'hidden button')
[*] console events : none delivered (Console domain disabled)
The closed-shadow-root win is real and convenient. The console silence is the price of the Console domain patch: with Patchright, page.on("console") never fires, so if your workflow leans on console scraping for debugging or data extraction, plan around it (write results into the DOM instead, which works from any execution world).
What Patchright Does Not Fix
A tool article that only shows green checks is marketing. Here is the honest remainder.
Headless is still headless. The HeadlessChrome UA token, the software GL fallback on GPU-less hosts, and the residual behavioral signature of not rendering to a screen are all yours to manage. The Patchright documentation’s recommended configuration - which we quote as their guidance, not our measurement - is:
p.chromium.launch_persistent_context(
user_data_dir="...",
channel="chrome",
headless=False,
no_viewport=True,
# do NOT add custom browser headers or user_agent
)
Real Google Chrome, headful, persistent profile, and - critically - no fingerprint injection, because a real Chrome presenting itself plainly is the most statistically common browser on the internet. Every spoof you add is a coherence risk of the kind we documented in the Camoufox article. If you must run headless, Xvfb-style virtual displays or the new headless mode with channel="chromium" are the middle grounds, and configuration 4 above is the honest floor.
The engine says Chromium no matter what. The mirror image of the Camoufox limit: Patchright drives Chromium, so fingerprint probes that characterize V8, Blink, or the Chrome-specific object graph see exactly a Chromium - which is usually what you want, but it cannot present as anything else, and its Chromium build version is visible.
Init scripts have a documented timing exposure. Patchright’s route-based init-script injection is, in the project’s own words, detectable by timing attacks that “no antibot currently checks.” We report that as the project’s claim; we did not construct such a timing attack ourselves. Treat it as accepted residual risk, not as zero risk.
Everything above the browser is untouched. IP reputation, TLS and HTTP/2 fingerprints, behavioral biometrics, and the economic-adversarial layer we described in our article on Cloudflare’s Adaptive Intelligence all still apply. Patchright fixes the driver layer, full stop. The IP layer is a separate axis - any proxy works with it, and the right exit class depends on your target’s paranoia, exactly as laid out in the Camoufox article’s IP discussion.
“Undetectable” is a date-stamped claim. The project’s README lists a roster of vendors its setup currently passes (Cloudflare, Kasada, Akamai, DataDome, Fingerprint.com, and others). Those are the maintainers’ claims about their configuration at their point in time; vendors run Patchright against their own detectors too, and yesterday’s bypass is tomorrow’s signature. Pin your version, and re-run a probe like ours against your actual targets after every upgrade - including checking that the CDP probe stays silent, because upstream Playwright changes can break patch assumptions in both directions.
Choosing Your Weapon
Where this leaves the toolkit decision, three articles in:
| Situation | Reach for | Why |
|---|---|---|
| Target probes JS engine internals, or you need Firefox presentation | Camoufox | Engine-level spoofing, coherent identity generation, geoip alignment |
| Target is Chromium-typical; you want stock-browser fingerprints + selectors | Patchright | Stock Chrome fingerprints, driver-level leak closure, closed shadow roots |
| Raw speed at scale, no JS rendering needed | TLS-impersonating HTTP clients | No browser, no browser leaks; pair with the coherence lessons |
| You are already flagged | Slow down, reconsider target and rate | No tool undoes reputation; avoid the CAPTCHA instead of solving it |
The deeper point these three articles converge on: detection is layered, so evasion must be layered, and each layer has a tool that owns it. The driver layer - the subject of this article - is the one most often left unpatched, because its leaks are invisible to the scraper and trivially visible to the defender.
Responsible Scraping
Same rules as always: these techniques are for targets where you have a legitimate data-collection right and the anti-bot layer is the only obstacle. Respect terms of service and robots where your situation requires it, do not break authentication boundaries, throttle to plausibly human load, and prefer licensed APIs and datasets when they exist. A patched driver changes how detectable you are, not what you are entitled to collect.
Sources
- Patchright - patched Playwright driver: https://github.com/Kaliiiiiiiiii-Vinyzu/patchright
- Patchright Python package: https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python
- Patchright driver patch files (chromiumSwitchesPatch.ts, chromiumPatch.ts): https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/tree/main/driver_patches
- Rebrowser - patches and leak documentation for Puppeteer/Playwright: https://github.com/rebrowser/rebrowser-patches
- Rebrowser - “How to fix Runtime.Enable CDP detection of Puppeteer, Playwright and other automation libraries”: https://rebrowser.net/blog/how-to-fix-runtime-enable-cdp-detection-of-puppeteer-playwright-and-other-automation-libraries-61740
- Antoine Vastel / DataDome - “How New Headless Chrome & the CDP Signal Are Impacting Bot Detection”: https://datadome.co/threat-research/how-new-headless-chrome-the-cdp-signal-are-impacting-bot-detection/
- Brotector - automation detection test page (and source): https://ttlns.github.io/brotector/
- bot.sannysoft.com - browser automation detection test: https://bot.sannysoft.com/
- Playwright documentation: https://playwright.dev/python/
- Chrome DevTools Protocol - Runtime.enable: https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-enable
- MDN Web Docs - Navigator.webdriver: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/webdriver
- Hunt-Benito - The Fingerprint That Hangs Together (Camoufox): https://www.hunt-benito.com/blog/the-fingerprint-that-hangs-together-coherent-identity-spoofing-with-camoufox/
- Hunt-Benito - The Rule That Vanishes (Cloudflare Adaptive Intelligence): https://www.hunt-benito.com/blog/the-rule-that-vanishes-cloudflares-adaptive-intelligence-and-the-new-economics-of-scraping/
- Hunt-Benito - Type What You Hear (CAPTCHA bypass history): https://www.hunt-benito.com/blog/type-what-you-hear-a-history-and-evolution-of-captcha-bypass/