VoidCrawl uses a minimal-footprint stealth strategy inspired by undetected-chromedriver◑, zendriver△, and nodriver○. Stealth is enabled by default. The guiding rule: present a real browser consistently — don’t fake things.
At a glance
Layer
What VoidCrawl does
Why
Launch flags
Drop chromiumoxide’s --enable-automation / --disable-extensions; add --disable-blink-features=AutomationControlled + zendriver flags
The automation signal is in the flags, not JS. AutomationControlled makes navigator.webdriver a native false — we do not patch it in JS.
No JS injection
addScriptToEvaluateOnNewDocument is empty
Every injected script is itself a fingerprint. We patch nothing in page-world JS.
UA / Client Hints
Real UA (any Headless token stripped), with navigator.platform + userAgentData derived from that UA so they agree
A Linux UA with platform === "Win32", or empty brands, is a bot tell.
Legacy headless renders WebGL with SwiftShader (software) — a strong bot signal.
Against managed Cloudflare Turnstile (the hard case): headful passes non-interactively (verified server-side, siteverify success:true, interactive:false); headless is still gated. Every default below is overridable — see Overriding the defaults.
Philosophy: less is more
Most browser automation tools try to spoof every fingerprint signal — fake plugins, fake WebGL, fake user-agent strings. This backfires against modern WAFs (Akamai, Cloudflare, PerimeterX) because:
Spoofed values are inconsistent. A hardcoded Chrome/131 user-agent on a system running Chromium 148 is an instant flag. A fake WebGL renderer that doesn’t match the real GPU is trivially detected.
The spoofing itself is detectable. Each Page.addScriptToEvaluateOnNewDocument CDP call is a fingerprint. Overriding navigator.plugins with a Proxy/getter behaves differently from the real PluginArray prototype.
The automation signal isn’t in JS — it’s in Chrome’s launch flags. chromiumoxide’s default flags include --enable-automation, which tells every WAF “I’m automated” before any page loads.
VoidCrawl’s approach: don’t fake anything. Launch with clean flags, let Chrome report its real values, and only ensure those values are internally consistent.
Lesson learned the hard way
VoidCrawl used to inject two JS patches — and both were removed because they hurt:
Deleting navigator.webdriver made it undefined. Real Chrome reports false; undefined is itself the tell.
Force-opening shadow DOMs broke Cloudflare Turnstile, which renders its challenge in a closed shadow root and tamper-checks it. Forcing it open failed the challenge with ERROR 600010.
VoidCrawl injects zero page-world JS today.
The automation signal is in the launch flags
After disable_default_args() (which strips chromiumoxide’s toxic defaults) VoidCrawl re-adds a curated set.
Removed (toxic flags)
Flag
Why it’s bad
--enable-automation
Literally opts in to automation detection
--disable-extensions
Normal Chrome always has extensions support
Added (zendriver flags)
Flag
Purpose
--disable-blink-features=AutomationControlled
Removes the automation-controlled blink feature -> navigator.webdriver is a native false
Suppress automation-ish background behavior and UI
Plus the safe noise-reducers (--disable-background-networking, --disable-breakpad, --disable-dev-shm-usage, --no-first-run, …).
UA / platform / Client-Hints consistency
The only override VoidCrawl applies is via CDP Emulation.setUserAgentOverride (not page-world JS). It probes the browser’s real UA, strips any Headless token, and derives a coherent identity from that one string so UA, navigator.platform, and navigator.userAgentData all agree:
Signal
Value (for a real Linux/Chrome UA)
navigator.userAgent
real build, HeadlessChrome -> Chrome
navigator.platform
Linux x86_64 (Win32 / MacIntel for those UAs)
Sec-CH-UA-Platform
Linux / Windows / macOS
userAgentData.brands / fullVersionList
Chromium / Google Chrome at the UA’s version + a GREASE entry
A mismatch here — e.g. a hardcoded platform: "Win32" on a Linux UA, or empty brands — is itself a bot signal. Both are flagged by the rebrowser bot-detector▢, and now pass clean.
GPU acceleration (hardware WebGL)
A headless browser that renders WebGL with SwiftShader (Chrome’s software fallback) advertises itself: WEBGL_debug_renderer_info returns "ANGLE (... SwiftShader ...)", which Cloudflare and others weigh as “no real GPU -> likely a bot/VM.”
VoidCrawl forces hardware rendering:
--headless=new — the legacy --headless forces SwiftShader; the new mode runs the full browser stack and can use a real GPU.
--use-angle=vulkan + --enable-gpu + --ignore-gpu-blocklist — route WebGL through ANGLE on the real GPU.
--disable-gpu-sandbox — lets the GPU process reach the DRM render node. This is the lever — on a host with a working driver it’s usually all you need.
The defaults are vendor-generic: ANGLE uses whatever Intel/AMD/NVIDIA driver the machine has. Verified on AMD, the renderer becomes ANGLE (AMD, Vulkan ... RADV ...), radv) — hardware, not SwiftShader.
GPU in Docker
Inside a container, hardware GPU also needs Mesa drivers in the image and/dev/dri passthrough — see Docker. Without a GPU passed through, the container falls back to SwiftShader. NVIDIA needs the nvidia-container-toolkit instead of /dev/dri.
What VoidCrawl does not touch
It injects no page-world JS, and leaves these alone:
Signal
Why
navigator.webdriver
The launch flag already yields a native false. A JS patch (delete -> undefined, or a redefined getter) is itself detectable.
navigator.plugins
Real Chrome populates it; faking creates inconsistencies.
navigator.userAgent
Real UA (Headless stripped) — no version mismatch.
Default behavior is already correct; spoofing adds detectable noise.
Shadow DOM mode
We do not force-open it (it broke Turnstile). Clicking a challenge widget works via real compositor clicks at pixel coordinates regardless of shadow mode.
Headful vs headless (and managed Turnstile)
WAF and managed-Turnstile targets require headful mode
Headless Chrome has fundamental differences sophisticated WAFs detect regardless of any patch:
Different rendering/compositing pipeline.
Missing / non-default screen, media, and input properties.
A lower managed-challenge score.
For Akamai, Cloudflare managed Turnstile, and similar, use headful via Docker & VNC.
Verified against managed Cloudflare Turnstile with a real sitekey (server-side siteverify):
For a headless farm that still needs to clear Turnstile, run the headful GPU container (Docker headful) rather than headless.
Overriding the defaults
Every default flag is overridable — force a GPU backend, disable acceleration, add a proxy bypass. Caller extra_args are merged by switch key: a caller value replaces the matching default (VoidCrawl does not emit duplicate switches, since Chrome’s per-switch precedence is inconsistent).
from voidcrawl import BrowserConfig
# Force software rendering (e.g. to compare, or on a GPU-less box):
networkIdle fires after zero in-flight requests for 500ms — but WebSockets, SSE/long-polling, analytics beacons, and lazy-loading keep the network active, so on many SPAs it never fires. Prefer wait_for_selector("<the element you actually care about>") for those.
Disabling Stealth
from voidcrawl import BrowserConfig, BrowserSession
asyncwithBrowserSession(BrowserConfig(stealth=False)) as session:
Note that the clean launch flags (including the GPU and anti-automation set) apply regardless of stealth; the toggle only governs the per-page UA/Client-Hints override.
Real-World Results
Target
Approach
Result
Akamai WAF (BusinessWire)
chromiumoxide defaults (--enable-automation)
403 Access Denied
Akamai WAF (BusinessWire)
+ heavy JS spoofing + fake UA
403 Access Denied
Akamai WAF (BusinessWire)
clean flags + real UA, no JS injection
Success (600K chars)
Managed Cloudflare Turnstile (real sitekey)
headful, hardware GPU, consistent UA, no JS
Pass (siteverify success:true)
Managed Cloudflare Turnstile
headless
Gated (before-interactive)
The lesson, twice over: the flags plus a consistent real browser matter more than JS patches — and a wrong JS patch is worse than none.
FAQs
Will stealth mode bypass all bot detection?
No. VoidCrawl (headful) clears common WAFs and managed Cloudflare Turnstile. Headless is still gated by managed Turnstile, and protections relying on IP reputation, residential-only access, or interactive CAPTCHAs may still require a proxy or a human.
Does stealth add latency?
Negligible. There is no per-tab JS injection anymore; the only per-tab step is a single CDP setUserAgentOverride. Flag changes are applied at Chrome launch with zero runtime cost.
Why not use a custom user-agent?
A custom UA that doesn’t match the actual Chrome version is an instant detection signal. VoidCrawl uses Chrome’s real user-agent (stripping only any Headless token) and makes navigator.platform + Client Hints agree with it.