Launch identity keeps BetterWright's managed browser coherent across the two layers that sites score. It replaces the old "Cloaking V2" layer; the browser is always the managed BetterChromium fork (see chromium-fork.md). For a browser that is not the managed fork, see browser-providers.md.
- Chromium layer - the fork's source-level patches, a stable fingerprint seed, and native locale/timezone flags.
- IP layer - an optional upstream egress proxy chained through the local policy guard, with locale/timezone resolved to match the egress geography.
The model API is unchanged: snippets, snapshots, human.*, captcha.*,
credentials, and proof captures work as before.
The fork presents the host it runs on
The identity layer deliberately does not mask the operating system. A
Linux host is a Linux browser: navigator.platform, the UA, UA-CH client
hints, screen geometry, and GPU strings all report the real platform, kept
coherent by the fork's source patches. Masking a Linux process as a consumer
Mac made every mismatch (fonts, WebGL renderer, media codecs, worker
behavior) a separate tell; the fork's patches keep a headless Linux browser
coherent as a Linux desktop browser instead. The explicit platform option
remains for pinning a specific identity, but nothing defaults away from the
host.
The identity layer does not monkey-patch browser APIs in the page world. The managed binary remains authoritative for canvas, WebGL, audio, fonts, plugins, hardware, automation markers, and client hints.
| Surface | Behavior |
|---|---|
| Geometry | Native fork geometry; headed-invisible parks a real window off-screen |
| Locale | --lang, --fingerprint-locale, and Accept-Language |
| Timezone | --bw-timezone, so Intl and Date agree |
| Platform | --fingerprint-platform=<host> — the real host OS |
| GPU, plugins, hardware | Fork source-level fingerprint patches |
| JavaScript APIs | Left native; no replacement getters or functions |
| Service workers | Blocked in new contexts while default-on ad blocking is enabled; allowed when it is disabled. Existing remote contexts have coverage limits |
In the recorded identity comparison, forcing page-world values made a synthetic
probe look perfect, but live reCAPTCHA checks stalled. Leaving the APIs native
restored server-verified 0.9 in both true-headless and headed modes. These
were demo observations, not an acceptance guarantee for future runs or sites.
Launch modes
- Headless (CLI default): a real
--headlessfork process with a realistic desktop viewport. This is not an off-screen headed substitute. - Headed (
--headed): a normal visible browser window. - Headed-invisible (
headedInvisible: true/--headed-invisible): a headed window parked off-screen for workflows that explicitly need native window compositing.
The JavaScript SDK instead defaults to headless: "auto": headed when a
display is available, headless otherwise. Pass true or false to pin it.
See display detection.
Egress proxy
const browser = new BetterWright({
upstreamProxy: "socks5://user:pass@residential-egress:1080",
geoip: true,
});
- Supports
http://andsocks5://upstreams with optional credentials. - Every connection is still policy-checked and DNS-validated locally.
- The guard tunnels to the validated literal IP, preserving DNS-rebinding protection while the target observes the upstream IP.
- WebRTC is forced onto the proxied path.
geoip: trueresolves locale/timezone through the upstream. Explicitlocaleandtimezonevalues always win.
Verification
Build the runtime and type-check the harness first with bun run build:harness.
bun research/stealth-report.ts
bun research/stealth-report.ts --live
bun research/stealth-report.ts --live --site recaptcha-v3-score
The local fixture checks roughly 30 browser surfaces against stock-Chrome behavior. The table records earlier runs against server-verified score endpoints; it is not a verification of the current release or a promise that another session will receive the same scores:
| Check | True headless | Headed |
|---|---|---|
| Local fixture | stock-Chrome parity | stock-Chrome parity |
antcpt.com/score_detector | 0.9 | 0.9 |
democaptcha.com | 0.9 | 0.9 |
recaptcha-demo.appspot.com was not an acceptance gate in those runs. Its widget
reported that the site exceeded its Enterprise free quota, so an unresolved
request there did not establish browser detection.
Interactive CAPTCHA issuance is a separate gate. In those recorded public-demo
runs, Turnstile remained at processing without a token in both modes, while
reCAPTCHA v2 escalated to an image grid that requires vision. The report prints
tokenIssued explicitly so those outcomes cannot be mistaken for a pass.
Configuration
| Option | Default | Meaning |
|---|---|---|
launchIdentity | true | Coherent locale/timezone identity layer. CLI: --no-launch-identity |
upstreamProxy | - | http:// / socks5:// egress proxy. CLI: --upstream-proxy |
geoip | false | Locale/timezone from egress IP. CLI: --geoip |
locale | - | Explicit BCP 47 locale. CLI: --locale |
timezone | - | Explicit IANA timezone. CLI: --timezone |
platform | host | Identity platform pin (default: the real host OS). CLI: --platform |
headedInvisible | false | Off-screen headed window. CLI: --headed-invisible |
stealthRuntimeFix | false | Optional patchright isolated-world execution |