How bot detection works — and why Clearcote patches the engine
The usual approach patches navigator.webdriver, spoofs the WebGL vendor, and overrides navigator.plugins from script. Detectors still flag it — and the reason is structural, not one more property left uncovered.
A JavaScript spoof self-reveals
A JS spoof is a function standing where a native one belongs. A detector sets the returned value aside and interrogates whether the thing returning it is native:
| The tell | Why it catches a JS spoof |
|---|---|
toString self-reveal | A native method stringifies to function get vendor() { [native code] }; an override stringifies to its own source — one .toString() catches it. |
Descriptor / hasOwnProperty | getOwnPropertyDescriptor exposes redefined props, and hasOwnProperty('toString') returns true on a tampered function where a native one returns false. |
Wrong-this TypeError | Native getters throw a specific TypeError on the wrong receiver; a naive shim stays quiet, and the silence is the signal. |
| Realm re-acquisition | A detector grabs a pristine Function.prototype.toString from a fresh iframe or Web Worker and turns it on your getter — a different realm from your main-world patch. It returns your source. Caught. |
Clearcote has no such layer. The getter for navigator.userAgent is the C++ getter: it reports [native code] because it is native code, identical across every realm — main frame, iframe, and worker. There is no JavaScript hijacking to detect.
The three layers of bot detection
Modern anti-bot systems read three structurally different surfaces, in three separate places. One tool rarely fixes all three:
| Layer | The tells | Where the fix lives | Clearcote |
|---|---|---|---|
| A · driver / binary | cdc_ ChromeDriver vars, the WebDriver protocol surface | Drive raw CDP, skip chromedriver | ✅ a plain Chromium binary — no driver artifacts |
| B · CDP side-effects | Runtime.enable leaks, injected init-scripts, main-world execution, automation-default viewport | The control / CDP-client layer | ✅ the engine neutralises the Runtime.enable leak, the SDK injects no scripts by default, and launch defaults avoid the automation-default viewport (where your client runs its own scripts is up to it) |
| C · fingerprint surface | canvas, WebGL, audio, fonts, navigator, TLS — across main frame, iframes, workers | The engine (C++), because JS overrides self-reveal | ✅ this is Clearcote |
The thing that matters most: no spoof-vs-real seam
Because the controls live in the engine, the JavaScript a page sees and the network handshake underneath it come from one real Chromium. There is no spoofed-JS-over-real-TLS seam for a cross-check to catch — the exact failure mode that gives injection-based tools away. One --fingerprint seed produces a single, internally consistent machine across canvas, WebGL, audio, fonts and hardware. The TLS and HTTP/2 handshake underneath is the engine's own, and it agrees because the persona reports the version the engine really is — leave brandVersion unset to keep it that way.
And when the noise itself is the tell, switch it off — canvas and WebGL return their natural values while the identity spoof stays on (rendered audio and client rects are never perturbed either way, and the AudioContext rate/latency scalars keep following the persona; here is why):
await launch({ fingerprint: "u1", fingerprintNoise: false }); // identity on, per-site farbling offWith the noise off, identities on the same machine share one canvas hash. canvasNoise: false turns off canvas 2D only and keeps the WebGL noise (licensed build, 150 r12+).
Network and locale coherence
The persona is only half of it: the timezone, language and WebRTC address also have to agree with where the traffic comes from. Behind a proxy, launch with geoip — the SDK looks up the proxy's exit IP and sets the timezone, language list, geolocation and WebRTC address for that region, and stops the launch if it can't resolve one. Without it, the timezone follows the language (en-US means New York), whatever country the proxy is in. See Playwright & Puppeteer for the options.
This is why Clearcote patches the C++ engine instead of injecting JavaScript. See Fingerprint flags for what you control, and Architecture for how the engine keeps every signal coherent together.
Related reading