We ran nine browser automation approaches through CreepJS and recorded what the detector actually saw. Playwright-stealth 1.0.6 holds Headless at 100% and raises Stealth-detection to 40%. Puppeteer-stealth drives Stealth-detection to 80%. Only camoufox scores 0% across all three categories. The data leads here: most patch-based stealth libraries don't fix headless detection. They trade it for a 'this is a stealth library' detection signal. CDP-direct architectures and different-engine spoofing are the only approaches that meaningfully push CreepJS scores down.
The Stealth Library You Installed in 2022
playwright-stealth from 1.0.5 to 1.0.6. The README lists 29 patches it applies. More delays, longer jitter windows, reduced concurrency. Still failing. The proxies are clean. The fingerprint is clean. The stealth library is installed. Nothing changed.Here is what actually changed: the site updated its detection rules, and the updated rules include a check for the exact set of patches that
playwright-stealth applies. Your "stealth" library left a signature that reads, to CreepJS and to any detection stack built on similar primitives, as a known automation tool. You didn't hide. You announced yourself in a different voice.This is the paradox at the centre of patch-based stealth. Every library that patches
navigator.webdriver, spoofs navigator.plugins, overrides chrome.runtime, and resets the permissions API creates a fingerprint of its own patches. The more thorough the patching, the more specific the fingerprint.We ran nine configurations through CreepJS (a widely-used open-source detection tool that scores browser environments across dozens of signals) and captured the results. The full matrix is in §4. What it shows is not that stealth libraries fail to patch the things they patch. They do patch them, mostly. What they fail to do is prevent the detector from classifying the resulting environment as a known automation tool by a different route: the specific combination of patches present.
Most patch-based stealth libraries don't fix headless detection. They trade it for a "this is a stealth library" detection signal. CDP-direct architectures and different-engine spoofing are the only approaches that meaningfully push CreepJS scores down.
The architecture you use matters more than the patches you apply, and no amount of patching on top of a CDP-detected framework will change that.
What CreepJS Actually Checks in 2026
Group them into five buckets and the picture sharpens.
Navigator-stack tells. The
navigator.webdriver flag is the most obvious tell — it is true by default in any CDP-connected browser and the easiest to patch. Below it are the signals most libraries ignore: navigator.plugins length (real Chrome ships 5; headless Chrome ships 0 or a fake array), navigator.languages (headless defaults to a single-element array, real browsers send 3–6), the Permissions API (reports "granted" for notifications in headless even when no permission dialog is possible), and chrome.runtime presence (undefined in any non-Chrome or patched environment). Patch one; the others still fire.Fingerprints. Canvas hash, WebGL vendor and renderer string, audio-context output hash, font enumeration count, voice synthesis list, and TLS handshake profile. These are stable across sessions and differ measurably between headless Chrome on a server (no GPU, no physical audio card, no installed fonts) and a desktop browser on the same machine. Our lab's Windows 11 box exposes 215 fonts to Chromium-based libraries and 118 to Camoufox (Firefox enumerates differently); a bare server-side Chrome instance with no installed desktop fonts typically reports under 20. Most stealth libraries do not touch the WebGL renderer: the string
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE), Google) announces itself.Headless-specific tells.
MediaCapabilities.decodingInfo returns mismatched values when no GPU is present. screen.colorDepth, devicePixelRatio, and window geometry can mismatch the reported display. navigator.hardwareConcurrency above 16 on a headless instance is rare on real desktops and statistically suspicious.Lies. CreepJS calls these "lie detectors". It compares browser-API values against each other: if
navigator.userAgent says Chrome 124 but chrome.runtime is undefined or shimmed, that contradiction is flagged. It also tests Date.now() and Math.random() for deterministic output, which appears in certain replay-based frameworks.What CreepJS does not measure. Behavioural signals are out of scope: mouse-path entropy over time, scroll velocity distribution, keystroke timing. So is cross-session fingerprint correlation. CreepJS is a single-page, single-session probe.
Most stealth libraries address bucket one. The other four are where the gap shows up.
The Libraries on Test
Headed Chrome is the control. No system Chrome on the lab box, so this row is Playwright running in headed mode: visible window, real GPU path, no headless flags. It sets the upper bound for what a non-headless environment looks like under CreepJS.
Headless Chrome via raw CDP skips every automation framework. The browser starts with
--headless=new and pychrome drives it directly over the DevTools protocol. No Selenium. No Playwright. This isolates what pure headless exposure costs, without any patching layer.Vanilla Playwright is the default automation experience: no extra plugins, no init-script overrides. It is the baseline for the Playwright family.
Playwright-stealth 1.0.6 is the canonical 2022-era patch library. It applies 29 overrides via
page.addInitScript. Getting it to run on Playwright 1.49 required venv-side patches to work around scope-isolation in addInitScript; the upstream library does not document this.Vanilla Puppeteer is the Node counterpart to vanilla Playwright: same Chrome binary, different API surface.
Puppeteer-extra-plugin-stealth 2.x is the Node twin of playwright-stealth, built on top of the
puppeteer-extra plugin system.Undetected-chromedriver 3.5.5 patches Chrome at the binary level to strip WebDriver-detected flags. Selenium-era approach, still maintained.
Nodriver 0.46.2 is the same author's successor: CDP-direct, no Selenium, no WebDriver. It shares DNA with undetected-chromedriver but cuts the driver layer entirely.
Camoufox 0.4.11 runs Firefox, not Chrome. Different rendering engine, different fingerprint surface, different TLS stack. The engine swap is the entire strategy.
All captures ran on 2026-05-06, Windows 11, Python 3.13, Node 22 LTS. Here is what we got.
The Matrix: Nine Libraries, One CreepJS Battery
Three findings stand out. Camoufox 0.4.11 is the only entry at 0/0/0/false. It runs Firefox; none of the Chromium-specific probes apply. The score is not a patching achievement; it is an engine mismatch.
Playwright-stealth 1.0.6 does not reduce Stealth-detection relative to vanilla Playwright. It raises it from 0% to 40%. The 29 patches produce a known fingerprint, and CreepJS classifies that fingerprint as a stealth tool. You installed an anti-detection library and it made you more detectable by a different column. Puppeteer-extra-plugin-stealth 2.x lands at 80% on the same column.
The CDP-direct cluster (headless-cdp, undetected-chromedriver 3.5.5, nodriver 0.46.2) all converge at 19/67/0/false. Removing the WebDriver layer clears the navigator stack (
webDriverIsOn drops to false), but GPU and font signals still fire, holding Headless at 67%.| Library | Like Headless | Headless | Stealth-detected | webDriverIsOn |
|---|---|---|---|---|
| headed-chrome (Playwright headed; no system Chrome on lab) | 31% | 33% | 0% | true |
| headless-cdp (raw CDP, no framework) | 19% | 67% | 0% | false |
| playwright-vanilla | 88% | 100% | 0% | true |
| playwright-stealth 1.0.6 | 81% | 100% | 40% | true |
| puppeteer-vanilla | 25% | 100% | 0% | true |
| puppeteer-extra-plugin-stealth 2.x | 25% | 33% | 80% | false |
| undetected-chromedriver 3.5.5 | 19% | 67% | 0% | false |
| nodriver 0.46.2 | 19% | 67% | 0% | false |
| camoufox 0.4.11 | 0% | 0% | 0% | false |
Patching is additive; engine swapping is subtractive. Every patch applied on top of a detectable framework adds surface area to the fingerprint it claims to hide.
Cross-Validation: browserleaks
Four findings from the data:
Both playwright-vanilla and playwright-stealth 1.0.6 show
ANGLE (Google, Vulkan 1.3.0 SwiftShader) as the WebGL renderer. SwiftShader is Google's CPU software renderer, present when Chrome runs without a GPU. Playwright-stealth patches 29 navigator properties and zero WebGL strings. Identical canvas hash, identical renderer: the stealth library changed nothing at this layer.Puppeteer-extra-plugin-stealth 2.x spoofs the renderer to
Intel Iris OpenGL Engine, a macOS GPU backend string, on a Windows 11 box. A detection rule comparing renderer strings against the User-Agent OS will flag the contradiction immediately.Font count splits by engine: 215 for every Chromium library, 118 for Camoufox 0.4.11. Firefox enumerates fonts differently; the split is an engine byproduct, not a spoof.
At TLS, every Chrome-family library produces
t13d1516h2_8daaf6152771_*, varying only in the last segment. Camoufox produces t13d1717h2_5b57614c22b0_3cbfd9 — different cipher count, different extensions, different protocol entirely. No script-level patch touches the TLS handshake.WebRTC showed no leaks across all nine. A non-finding, noted once.
| Library | Canvas (12 hex) | WebGL renderer | Fonts | WebRTC | navigator.webdriver | JA4 prefix |
|---|---|---|---|---|---|---|
| headed-chrome | 3c82c3bc6d4b | Intel Iris Xe (ANGLE) | 215 | no leak | true | t13d1516h2_8daaf6152771_02713d |
| headless-cdp | 8acd12f70d92 | Intel Iris Xe (ANGLE) | 215 | no leak | false | t13d1516h2_8daaf6152771_d8a2da |
| playwright-vanilla | e4b78401a4ec | SwiftShader (Vulkan 1.3.0) | 215 | no leak | true | t13d1516h2_8daaf6152771_02713d |
| playwright-stealth | e4b78401a4ec | SwiftShader (Vulkan 1.3.0) | 215 | no leak | true | t13d1516h2_8daaf6152771_02713d |
| puppeteer-vanilla | 8acd12f70d92 | Intel Iris Xe (ANGLE) | 215 | no leak | true | t13d1516h2_8daaf6152771_d8a2da |
| puppeteer-stealth | 8acd12f70d92 | Intel Iris OpenGL (macOS-style on Windows) | 215 | no leak | false | t13d1516h2_8daaf6152771_d8a2da |
| undetected-chromedriver | 8acd12f70d92 | Intel Iris Xe (ANGLE) | 215 | no leak | false | t13d1516h2_8daaf6152771_d8a2da |
| nodriver | 8acd12f70d92 | Intel Iris Xe (ANGLE) | 215 | no leak | false | t13d1516h2_8daaf6152771_d8a2da |
| camoufox | 0ab29c0c02a1 | NVIDIA GeForce 8800 (spoof) | 118 | no leak | false | t13d1717h2_5b57614c22b0_3cbfd9 |
The cross-validation confirms the CreepJS ranking. The architectural order (camoufox alone, then CDP-direct, then patched stealth, then vanilla) holds across two independent probe families; the finding is not an artifact of how CreepJS scores, it is a property of the libraries themselves.
Anatomy of a Stealth Patch
page.addInitScript. The categories are well-documented in the maintained Python fork: redefine navigator.webdriver to false, fake navigator.plugins with a 5-element array, set navigator.languages to ['en-US', 'en'], override the chrome.runtime stub, hook navigator.permissions.query to return realistic results for the notifications permission, restore Object.getOwnPropertyDescriptor for several patched fields. Each one is a getter override or property redefinition injected before page scripts run.The problem is prototype-chain contamination. When you override a browser API with a JavaScript shim, the shim does not carry the
[native code] marker that real browser implementations do. CreepJS runs every patched property through toString() and checks whether the result contains [native code]. Patched libraries leave shims; real browsers do not. That inconsistency lands in CreepJS's "lies" bucket, and a cluster of lies from a recognisable set of properties reads as a known tool, not an unknown browser. The matrix confirms it: vanilla puppeteer-vanilla scores 100% Headless and 0% Stealth-detection. Apply puppeteer-extra-plugin-stealth 2.x and Headless drops to 33%, but Stealth-detection rises to 80%. Net detection went up. The library did not hide the automation; it replaced one detection vector with a stronger one.Playwright-stealth shows the same dynamic in a milder form: 0% Stealth-detection without the plugin, 40% with it. More patches, more fingerprint. The library advertises the act of patching.
Nodriver 0.46.2 takes the opposite approach. No init scripts. Nothing patches anything. The browser starts with
--headless=new; nodriver connects via raw CDP; navigator.webdriver is false because the WebDriver wire protocol was never activated, not because a shim overwrote it. The matrix shows nodriver at 19% Like Headless / 67% Headless / 0% Stealth-detection, identical to the raw CDP baseline. Removing the WebDriver layer entirely is structurally different from patching over it.Patches advertise themselves; absence does not. If a detector can identify that something patched the prototype chain, the patch is now the fingerprint.
The Signals Nobody Patches
GPU vendor and renderer string.
WebGLRenderingContext.getParameter(UNMASKED_VENDOR_WEBGL) and UNMASKED_RENDERER_WEBGL return what the OS reports from the graphics driver. On Playwright headless the renderer is ANGLE (Google, Vulkan 1.3.0 SwiftShader Device (Subzero), Google): Google's CPU software renderer, present when Chrome starts without a physical GPU. No JavaScript patch produces a convincing real GPU string; the function is gated by the actual driver call. The browserleaks data confirms it: playwright-vanilla and playwright-stealth 1.0.6 return the identical SwiftShader string. The stealth library patched 29 properties and zero WebGL parameters. Puppeteer-stealth spoofs Intel Iris OpenGL Engine, a macOS backend, on our Windows 11 box — introducing a contradiction is worse than saying nothing.Font enumeration. Our Windows 11 lab box exposes 215 fonts to every Chromium library; Camoufox enumerates 118 because Firefox uses a different font-query path. A server-side Chrome with no installed desktop fonts typically reports under 20. Patching this would require shipping a font database and intercepting enumeration calls; getting it wrong creates a different tell, so nobody does it.
Audio-context output.
AudioContext.createOscillator produces a deterministic byte sequence that differs by platform. Headless Chrome on a Linux server emits one hash; Chromium on a Windows desktop emits another. The delta appears at the third decimal place. Spoofing this means synthesising the target sequence in a hook: non-trivial, and easy to get wrong in ways that produce a new tell.Performance-timing entropy. Automation pipelines tend to produce a tighter
domContentLoaded-to-load time distribution than real user sessions. CreepJS does not measure this directly, but multi-session detectors do.TLS handshake profile. The TLS fingerprinting post covers this layer in detail. Even nodriver's handshake matches plain Chromium; nodriver sits above the network stack and cannot change what the binary negotiates. Camoufox produces JA4 prefix
t13d1717h2_5b57614c22b0; every Chrome-family library produces t13d1516h2_8daaf6152771. Different cipher count, different extensions. That split is the "different-engine spoofing" half of the thesis confirmed at the protocol layer.Real hardware in a real headed browser is the only stack that reaches single-digit detection. Below the JS layer is where stealth libraries run out of road.
What Works in 2026
Top tier: real headed Chrome with a virtual display. Running Chrome in headed mode under Xvfb on Linux (or a hidden window on Windows) is the only Chromium approach that reaches single-digit detection scores. The matrix entry is headed-chrome at 31% Like Headless / 33% Headless / 0% Stealth-detection. The residual comes from the webdriver flag (Playwright wires it even in headed mode) and from performance-timing signals. This is the right architecture when you are driving per-session targets where one browser per request is acceptable: browser automation for checkout flows, account actions, low-volume scraping of high-value targets. It is the wrong architecture for high-throughput crawling. Spawning 50 or more concurrent headed browsers on a single box requires a GPU farm or software rendering fallback; at that scale the cost per page rises faster than the return.
Middle tier: nodriver or Camoufox. Both push CreepJS scores below every patched-stealth approach in the matrix. Camoufox 0.4.11 reaches 0/0/0/false because it runs Firefox; none of the Chromium-specific probes fire. Nodriver 0.46.2 sits at 19/67/0/false; it clears Stealth-detection entirely because it applies zero init-script patches, but GPU and font signals still contribute to the Headless column. Architecture, not patches, is what separates these from the tier below.
Pick between them on operational fit. Camoufox wins on the CreepJS score and on TLS fingerprint diversity (JA4 prefix
t13d1717h2 vs Chrome's t13d1516h2). Nodriver (github.com/ultrafunkamsterdam/nodriver) wins on operational simplicity if your codebase is already Chromium-shaped and a Python rewrite to Firefox is too costly. Camoufox (github.com/daijro/camoufox) is the right call if you are starting fresh and the Firefox fingerprint database (thinner than Chrome's) matches your target population.Budget tier: playwright-stealth or puppeteer-extra-plugin-stealth. The data is plain: these clear targets that check basic bot.sannysoft.com signals. They do not clear Cloudflare Bot Management, Akamai Bot Manager, or DataDome. The 80% Stealth-detection score on puppeteer-extra-plugin-stealth means a detector with the plugin's signature can flag your session on the first page load. For targets running enterprise bot management, budget-tier stealth libraries are not a cost saving. They are a way of announcing which library you are using.
Architecture is the budget; patches are the line item. Pick the cheapest tier that clears your specific target, and do not pay for tier two if tier three works.
When Stealth Libraries Are Still Useful
What stealth libraries actually clear. Sites running basic checks on
navigator.webdriver, navigator.plugins.length, and the presence of chrome.runtime are neutralised by playwright-stealth or puppeteer-extra-plugin-stealth in one line of setup code. That population is not small. Smaller e-commerce platforms, classified ad sites, regional news outlets, internal dashboards at companies without a CDN bot-management contract: these run a lightweight JavaScript check, not a multi-signal fingerprint stack. Budget-tier libraries clear them cleanly and reliably.The cost picture. A playwright-stealth scraper takes minutes to configure and costs pennies per thousand requests. A real-headed-Chrome-with-Xvfb stack requires a display server and GPU allocation; software rendering fallback is slower, and CPU burn per concurrent browser climbs fast. If your target is comfortably in the long tail, budget-tier wins on operational economics by a wide margin. The CreepJS score of 100% Headless says nothing about whether your actual target site ever runs CreepJS or anything like it.
The right process. Tier choice should follow target profiling, not market positioning. Run one budget-tier request against the target. If it succeeds cleanly across 1,000 runs with no soft blocks, no intermittent 403s, no JS challenges, and no rate-limit ramp, you have your answer: budget-tier is sufficient and you are done. If you see soft-block patterns, upgrade to the middle tier and re-profile. There is no performance award for using the strongest tool on a target that would have accepted the weakest one. Proxies follow the same logic, and the web scraping with proxies guide covers the proxy-selection side of that decision.
The right tool is the cheapest one that clears your specific target, not the one with the lowest CreepJS score.
Methodology
tools/headless-capture/ and was destroyed after captures; that is lab policy, not caution about the tools themselves.Tool versions. Python 3.13.3 (Microsoft Store build); Node.js 22 LTS. Library versions pinned: Playwright 1.49.1 with Chromium 131.0.6778.33 (build 1148); playwright-stealth 1.0.6 (with venv-side patches to work around scope-isolation in
addInitScript on Playwright 1.49, which the upstream library does not document); Puppeteer 23.11.1 with its bundled Chromium 131.0.6778.204; puppeteer-extra-plugin-stealth 2.x; undetected-chromedriver 3.5.5; nodriver 0.46.2; selenium 4.26.1; pychrome 0.2.4; Camoufox 0.4.11 (Firefox-based, using its own bundled Firefox).Probes. CreepJS at
https://abrahamjuliot.github.io/creepjs/ and six browserleaks pages: canvas, webgl, fonts, webrtc, javascript, and tls at https://browserleaks.com/. The TLS page requires wait_until="domcontentloaded"; the page polls indefinitely and never reaches networkidle. All other pages use the default networkidle wait.What we did not use. No system Chrome was installed on the lab box. All Chromium-based libraries used either the Playwright-bundled Chromium 131 or Puppeteer's own bundled 131 build. Camoufox used its own bundled Firefox. We did not run system Chromium, headless Firefox outside Camoufox, Brave, or Selenium with vanilla chromedriver; nine libraries were chosen for typicality, not exhaustiveness.
Reproducibility. Each library script is approximately 50 lines and runs the same probe loop. The implementation plan contains the full scripts. Anyone can rerun on a clean Windows 11 box with:
pip install playwright==1.49.* playwright-stealth==1.0.* nodriver==0.46.* undetected-chromedriver==3.5.5 selenium==4.26.* pychrome==0.2.* camoufox==0.4.* && npm install puppeteer@23 puppeteer-extra@3 puppeteer-extra-plugin-stealth@2.Limitations. Single date; real measurements should aggregate over time. Single OS; Linux server-side captures may differ on font enumeration and GPU strings. Only nine libraries were tested. We do not name specific Cloudflare, Akamai, or DataDome bypass techniques; the captures probe CreepJS and browserleaks, both designed to be tested against.
Lab-hygiene disclosure. Everything installed inside
tools/headless-capture/ with its own venv and node_modules. Camoufox's bundled Firefox installs outside that directory (its installer ignores CAMOUFOX_HOME); the cleanup script removes it via the venv Python before destroying the sandbox. No proxies were used in any capture; all probes are direct connections. Databay's proxy network is not part of this experiment.