- snapshot: make collect_fingerprints private (TreeNode is private, so a
pub(super) fn leaked a more-private type)
- adaptive: if-let instead of single-arm match in attr_score
- stealth: move timezone test module to end of file (items-after-test-module)
No behavior change. Pre-release cleanup.
Borrow anti-detection hardening from Scrapling/patchright, preferring native
CDP/Chrome overrides over JS lies:
- Runtime.enable is now opt-in via AGENT_BROWSER_CAPTURE_CONSOLE (default off).
It was called on every session INCLUDING CdpAttach (the user's real Chrome),
leaking the patchright/rebrowser "runtime" CDP signal and undermining the
"real browser, no lies" guarantee. Runtime.evaluate/callFunctionOn and
runIfWaitingForDebugger work without it; only console/error capture needs it.
The console/errors commands now return a hint when capture is disabled.
- Timezone alignment via native Emulation.setTimezoneOverride, opt-in with
AGENT_BROWSER_TIMEZONE=<IANA>|auto (FullLaunch only). Intl and Date both
follow with no JS artifact.
- WebRTC IP-leak handling via the --force-webrtc-ip-handling-policy Chrome
flag: auto disable_non_proxied_udp when a proxy is set (so the real IP can't
leak past the proxy); AGENT_BROWSER_BLOCK_WEBRTC=1 hides the local IP when
there is no proxy; =0 opts out.
- Opt-in canvas/audio fingerprint noise via AGENT_BROWSER_HIDE_CANVAS=1
(FullLaunch only). Session-stable seed so reads stay consistent within a
session while differing from the headless-stable hash.
Adds 5 unit tests; full suite 751 passed, 0 failed.
Key insight: ANY JS-level modification to navigator.webdriver is detectable
by creepjs's lieProps system. The only undetectable approach is
Emulation.setAutomationOverride at the CDP protocol level, which tells
Chrome to natively return false for navigator.webdriver.
In CdpAttach mode, we now inject ZERO JavaScript patches — the browser's
real fingerprint is already perfect. Only the CDP protocol command is needed.
CreepJS results now match manual Chrome exactly:
- 0% headless (was 33%)
- 0% stealth (unchanged)
- 25% like headless (Chrome baseline, same as manual)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CreepJS detects three things for webDriverIsOn:
1. Property deletion (navigator.webdriver === undefined)
2. Value check (!!navigator.webdriver)
3. Lie detection (descriptor tampering via lieProps)
Changed from delete/defineProperty-value approach to replacing the CDP
getter with a getter returning false, matching the native descriptor shape.
Note: 33% headless in CreepJS is a CDP-inherent signal (lieProps detects
the getter replacement). This cannot be eliminated at the JS layer since
CDP sets the webdriver getter before init scripts run. Real-world impact
is minimal — Cloudflare Turnstile passes successfully.
Also confirmed: Chrome's remote_debugging preference in Local State
persists across restarts, so users only need to enable CDP once via
chrome://inspect/#remote-debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CdpAttach mode: only removes navigator.webdriver (user's real Chrome
already has genuine fingerprint, heavy patches create detectable lies)
- FullLaunch mode: applies all 32 patches (new Chrome needs full coverage)
- Improved webdriver removal: uses Object.defineProperty to override CDP
getter on Navigator.prototype, not just delete
- CreepJS results: 0% stealth (was 20%), hasIframeProxy: gone
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created cli/src/native/stealth.rs with stealth JS injection via CDP
- Extracted 32 patch IIFEs from TS stealth.ts into stealth_scripts.js
- Injected via Page.addScriptToEvaluateOnNewDocument on every launch/connect
- Added stealth Chrome args (disable AutomationControlled, use ANGLE GL)
- Auto-detects and cleans HeadlessChrome from User-Agent string
- Overrides navigator.userAgentData high-entropy hints
- Stealth enabled by default, disable with AGENT_BROWSER_STEALTH=0
Track 2 of native-stealth migration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>