Skip to content

Commit a861c00

Browse files
garrytanclaude
andauthored
v1.58.3.0 feat: gbrowser anti-detection Layer C stealth (garrytan#2047)
* feat: Layer C stealth — chrome.*, Notification, per-install hardware, toString Proxy (gbrowser T1+T3+D6) Three additions stacked into the existing applyStealth() init script to close the visible automation tells that today push GBrowser users into Google's /sorry/index captcha and similar: T1 — Strip Playwright's automation default args: --enable-automation (kills "Chrome is being controlled" infobar) --disable-popup-blocking, --disable-component-update, --disable-default-apps (Patchright's list — each is a documented tell) Now centralized in STEALTH_IGNORE_DEFAULT_ARGS export, used by BOTH launchHeaded() and handoff() (the headless → headed re-launch path). D6 — Drop "GStackBrowser" UA branding suffix: Real Chrome's UA ends `Safari/537.36`, not `Safari/537.36 GStackBrowser`. The branded suffix was a high-entropy classifier for any vendor that grep'd UA for known automation/test-browser strings. Branding still lives in the wrapper .app name + Dock icon + tray — does not need to leak via the UA string for the product to be "GBrowser." Resolves the "looks like Chrome but identifies as GStackBrowser" contradiction codex review garrytan#18 flagged. T3 — Layer C init-script additions in stealth.ts: 1. Function.prototype.toString Proxy (must run first). Wraps every patched getter / function in a WeakSet so they report `function NAME() { [native code] }` at every recursion depth, defeating the depth-3+ integrity check (fn.toString.toString.toString().includes('[native code]')). 2. window.chrome.runtime / chrome.app / chrome.csi / chrome.loadTimes restoration with full enum shape (OnInstalledReason, PlatformArch, PlatformOs, etc.) + method bodies. Real Chrome ships these; their absence is universally checked. Vendor research (gbrowser plan deep-dive on Cloudflare + DataDome) confirmed both vendors probe this shape directly. 3. Notification.permission aligned to 'default'. The existing inline addInitScript already spoofs permissions.query({name:'notifications'}) to return 'prompt' — Notification.permission being 'denied' while Permissions returns 'prompt' is a cross-source inconsistency that detectors flag specifically. 4. Per-install hardware values via GSTACK_HW_CONCURRENCY / GSTACK_DEVICE_MEMORY env vars (set by gbd's host_profile.go from system_profiler + sysctl). Reporting real host values within the Chrome shape avoids the cross-user GBrowser fingerprint cluster that hardcoded defaults would create. Codex review garrytan#10 flagged hardcoding as creating contradictions across Apple Silicon / Intel / UA-CH architecture. 5. Selenium 25-global cleanup + PhantomJS + NightmareJS + Watir + Playwright (__pwInitScripts, __playwright__binding__) static-name deletion. The inline block continues to handle the dynamic cdc_/__webdriver/__selenium/__driver prefixes. D7 (codex correction) kept: still do NOT fake navigator.plugins or navigator.languages. Synthesizing those triggers MORE consistency flags from modern fingerprinters than letting Chromium surface them natively. Test coverage: - 15 new tests in stealth-layer-c.test.ts covering: launch-flag exports, script structure, toString-Proxy installs first, every spoof present, hardware values interpolated from input (not hardcoded), Selenium global cleanup spot-check, no GStackBrowser leak in stealth payload, backwards-compat exports preserved. - All 8 existing stealth-webdriver tests still pass. - All 2 existing browser-manager-unit tests still pass. For GBrowser specifically: this is the gstack-side half of Phase 1 / T1 + T3 + D6 in the anti-detection plan. The gbrowser repo's submodule pointer bump will land alongside this. * feat: buildGStackLaunchArgs — Pack 1 cmdline-switch construction for gbrowser New stealth.ts export that turns the GSTACK_* env vars (already populated by gbrowser's gbd from host_profile.go) into the --gstack-* cmdline switches the Pack 1 Chromium patches read at WebGL getParameter, NavigatorUA::userAgentData, NavigatorConcurrentHardware::hardwareConcurrency, and NavigatorDeviceMemory::deviceMemory time. Wired into all three launchArgs sites: launch() (headless), launchHeaded() (real product path), and handoff() (headless → headed re-launch). Mapping: GSTACK_GPU_VENDOR → --gstack-gpu-vendor GSTACK_GPU_RENDERER → --gstack-gpu-renderer GSTACK_PLATFORM → --gstack-ua-platform (with mapping: MacARM/MacIntel → macOS, Win32 → Windows, Linux x86_64 → Linux) GSTACK_GPU_CHIPSET → --gstack-ua-model GSTACK_HW_CONCURRENCY → --gstack-hw-concurrency GSTACK_DEVICE_MEMORY → --gstack-device-memory Each switch is emitted only when its env var is non-empty — empty values fall through to the patch's "no override" path, which returns the real Chromium native value. Safe to ship on Chromium builds without the Pack 1 patches applied (zero behavior change). The patches themselves live in the gbrowser repo at chromium/patches/ {webgl-vendor-spoof,ua-client-hints-stealth,worker-navigator-stealth}.patch. Both halves (gstack arg construction + gbrowser C++ patches) must land + Chromium rebuild before the spoof reaches the WebGL/UA-CH/ hardware accessors. Currently dormant until then. Tests (browse/test/stealth-layer-c.test.ts): 7 new buildGStackLaunchArgs cases — empty env, all-populated, partial, platform mapping (MacARM/MacIntel/Win32/Linux), unrecognized platform fallthrough, vendor-with-spaces escape-safety. All 32 stealth/browser-manager tests pass. For GBrowser specifically: gstack-side half of the Pack 1 flag plumbing. gbrowser repo will bump the submodule pointer to this commit, then re-run bun run test/anti-bot/evidence-run.ts to verify creepjs's "33% headless" score drops after Pack 1 + Chromium rebuild. * feat: buildGStackLaunchArgs adds --gstack-suppress-prepare-stack-trace Pack 2 / B11 flag plumbing for the new error-preparestacktrace-stealth.patch in gbrowser/chromium/patches/. Always emit --gstack-suppress-prepare-stack-trace unless the caller explicitly sets GSTACK_CDP_STEALTH=off in the environment. Off by default in patch behavior (no-op without the C++ patch), so this is safe on stock Playwright Chromium too. Closes the Cloudflare canary trick where a page sets Error.prepareStackTrace and watches for it to fire during CDP serialization of a logged Error object. Tests: All 33 stealth/browser-manager tests pass. New cases: - GSTACK_CDP_STEALTH=off disables suppression - empty env still emits the always-on flag (count=1) - all-populated env now emits 7 flags (was 6) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(browse): enable Chromium sandbox on headed launchPersistentContext Mirrors v1.40.0.1 from main lineage (PR garrytan#1617). Cherry-picked onto gbrowser-anti-detection so the GBrowser submodule can consume the fix without waiting for main to merge. Playwright auto-adds --no-sandbox whenever chromiumSandbox !== true (playwright-core/lib/server/chromium/chromium.js:291-292). The headless chromium.launch() site set the option; the two headed sites (launchHeaded() and handoff()) did not. Every headed launch on macOS and Linux showed Chromium's yellow "unsupported command-line flag: --no-sandbox" infobar. shouldEnableChromiumSandbox() centralizes the Win32 / CI / CONTAINER / root heuristic that previously lived only in the headless path's explicit --no-sandbox push at :225. All three launch sites now use the helper, and six unit tests pin the policy across darwin, linux, win32, CI, CONTAINER, and root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * v1.40.0.2 fix(browse): Cmd+Q on managed Chromium stops triggering supervisor respawn Three browser.on('disconnected') handlers in browse/src/browser-manager.ts (launch, launchHeaded, handoff) each exited with a non-zero code on every disconnect, regardless of cause. Process supervisors that consume our exit code (gbrowser's gbd HealthMonitor in cmd/gbd/health.go) treated user Cmd+Q identical to a Chromium crash and respawned with exponential backoff, so the visible browser kept reappearing after the user closed it. Add resolveDisconnectCause(browser) that reads the underlying ChildProcess exitCode + signalCode (waiting up to 1s for the exit event if the disconnected event fired first). Exit code 0 + no signal = clean user quit; anything else = crash, signal-kill, or OOM. Wire the resolver into all three disconnect handlers: - launch() (headless): clean → exit 0, crash → exit 1 (was always 1) - launchHeaded() (headed): clean → exit 0, crash → exit 2 (was always 2) onDisconnect() cleanup callback still runs in both cases. - handoff() (re-launch): same as launch() via the helper. Preserve the per-path crash codes (1 vs 2) so any supervisor that differentiated headed vs headless crashes keeps working. Seven new unit tests in browse-manager-unit.test.ts cover the resolver across already-exited, signal-killed (SIGSEGV / SIGKILL), async exits, and null-browser inputs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(browse): apply stealth on every launch path + share automation-artifact cleanup handoff() built cmdline args but never called applyStealth, so a handed-off browser had no JS stealth (no webdriver mask, no chrome.* shape, no toString proxy). And the cdc_/Permissions cleanup shim lived inline in launchHeaded() only, so headless launch() reported Notification.permission='default' without the matching permissions.query='prompt' answer — the exact cross-source inconsistency the shim exists to prevent. Move the cleanup into AUTOMATION_ARTIFACT_CLEANUP_SCRIPT inside applyStealth so all three launch paths (launch, launchHeaded, handoff) get identical stealth, and call applyStealth(newContext) in handoff() before restoreState() navigates. A static tripwire in browser-manager-unit.test.ts fails CI if any launch path drops the applyStealth call again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(browse): make --gstack-suppress-prepare-stack-trace opt-in, not default-on buildGStackLaunchArgs() pushed the flag unless GSTACK_CDP_STEALTH=off, i.e. on-by-default — contradicting its own comment ("off by default, only for gbrowser builds"). The switch is read by a C++ patch that only exists in gbrowser; on stock Playwright Chromium it is an unknown switch. Flip to opt-in: emit only when GSTACK_CDP_STEALTH is on/1/true. gbd opts in by exporting GSTACK_CDP_STEALTH=on; stock installs leave it unset so the flag never reaches a Chromium that wouldn't understand it. Comment now matches code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(browse): correct stale stealth comments The file-level stealth.ts docstring claimed "we DON'T fake navigator.plugins" while the same file now ships EXTENDED_STEALTH_SCRIPT, which does fake plugins when GSTACK_STEALTH=extended. Clarify that Layer C (the always-on default) doesn't fake plugins and the opt-in extended mode does, as the documented "actively lies, may break sites" escape hatch. Also fix the launch()/launchHeaded() comments that said "mask navigator.webdriver only" — applyStealth (Layer C) also restores window.chrome.*, aligns Notification.permission, and sets per-install hardware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(browse): runtime + extended-mode coverage for the stealth blend The stealth tests were all static string-shape assertions; nothing executed the script in a real page. Add real-Chromium runtime checks via applyStealth + page.evaluate: - Layer C runtime: window.chrome.* rich shape, Notification.permission='default' paired with permissions.query notifications='prompt' (guards the shim now running on every path), and patched getters reporting [native code]. - Per-install hardware: navigator.hardwareConcurrency/deviceMemory reflect the GSTACK_* env profile. - Extended-mode blend: navigator.plugins is faked when GSTACK_STEALTH=extended, Layer C still wins window.chrome.runtime, and navigator.webdriver stays false (own-prop getter survives extended's prototype delete). - Persistent-context (launchHeaded/handoff) parity now uses a page created AFTER applyStealth — the old test checked pages()[0], which predates the init script, so webdriver was false only via the launch arg, not Layer C. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(browse): handoff() + launchHeaded() spread the shared STEALTH_LAUNCH_ARGS handoff() built its launch args from only ['--hide-crash-restore-bubble', ...buildGStackLaunchArgs()], omitting STEALTH_LAUNCH_ARGS — so a handed-off browser kept the --disable-blink-features=AutomationControlled tell that launch() and launchHeaded() strip. launchHeaded() also hardcoded the flag as a literal. Both now spread the shared constant, so the AutomationControlled flag lives in one place across all three launch paths. Tripwires: STEALTH_LAUNCH_ARGS spread into >= 3 sites (no inline literal) and STEALTH_IGNORE_DEFAULT_ARGS wired into both persistent-context paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(browse): drop dead HostProfile.platform, export test internals HostProfile.platform was set by readHostProfile but never read by buildStealthScript — the platform spoof is owned by the UA-CH cmdline switch in buildGStackLaunchArgs (which reads GSTACK_PLATFORM directly). Remove the dead field. Export readHostProfile and AUTOMATION_ARTIFACT_CLEANUP_SCRIPT so their clamp/shape invariants can be unit-tested. Correct the stale "25 Selenium globals" count comment and note the extended cdc_ scan is redundant-but-retained for standalone use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(browse): cover readHostProfile clamp, toString depth-3, chrome.* calls Pre-landing review coverage gaps: - readHostProfile clamps 0/negative/NaN/missing env to 8 (a deviceMemory=0 or NaN would be a glaring bot tell) — now asserted. - toString proxy survives the depth-3 recursion trick (fn.toString.toString.toString().includes('[native code]')), the headline claim that was only tested at depth-1. - chrome.csi() and chrome.loadTimes() are invoked (not just typeof-checked) and runtime.connect() throws the native-shaped "No matching signature" error. - AUTOMATION_ARTIFACT_CLEANUP_SCRIPT static shape (cdc_/__webdriver strip + notifications->prompt) as a hermetic backup for the live-Chromium pairing test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(browse): recreateContext() re-applies stealth (closes 4th un-stealth path) useragent and viewport --scale route through recreateContext(), which rebuilds the BrowserContext via newContext() — a fresh context with no init scripts. It never called applyStealth, so a routine useragent/viewport-scale command silently dropped webdriver masking, window.chrome.* shape, hardware spoof, and the cdc/Permissions cleanup on every restored page. Caught by the cross-model adversarial review (Codex) after the Claude pass and eng review missed it. Both the main and fallback paths now call applyStealth before any page is created. The launch-path tripwire is raised to >= 4 sites and now asserts the recreateContext() body specifically, so the regression class can't recur. Also documents the load-bearing trust assumption on buildGStackLaunchArgs / readHostProfile (GSTACK_* must be gbd-sourced, never page/remote data — the injection-safety argument depends on it) and the notifications-permission spoof tradeoff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.58.3.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: sync browser stealth docs to Layer C (v1.58.3.0) BROWSER.md "Stealth scope" still described the default as navigator.webdriver masking only; Layer C is now the always-on default across all four context-creation paths. Update the stealth-scope prose, the "What GStack Browser means" blurb (stock-Chrome UA, no GStackBrowser suffix, captchas can still get through at the CDP layer), the stealth.ts source-map line, and the env-vars table (GSTACK_STEALTH, GSTACK_CDP_STEALTH, GSTACK_GPU_*, GSTACK_PLATFORM, GSTACK_HW_CONCURRENCY/GSTACK_DEVICE_MEMORY + the explicit --gstack-* switches and ignoreDefaultArgs stripping). Correct the stale "narrows to navigator.webdriver masking only" premise on the open CDP-patch TODO (the TODO itself stays open — the CDP-protocol layer is still unaddressed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c7ae632 commit a861c00

11 files changed

Lines changed: 1190 additions & 138 deletions

BROWSER.md

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -527,10 +527,16 @@ window is being controlled.
527527
### What "GStack Browser" means
528528

529529
Not your daily Chrome — a Playwright-managed Chromium with custom branding
530-
in the Dock and menu bar, anti-bot stealth (sites like Google and NYTimes
531-
work without captchas), a custom user agent, and the gstack extension
532-
pre-loaded via `launchPersistentContext`. Your regular Chrome with your tabs
533-
and bookmarks stays untouched.
530+
in the Dock and menu bar (the `.app` name, Dock icon, and tray, NOT the UA
531+
string), always-on Layer C anti-bot stealth (most JS-observable automation
532+
tells are masked, so many anti-bot-protected sites load cleanly), a
533+
stock-Chrome user agent that reports the underlying Chromium version, and the
534+
gstack extension pre-loaded via `launchPersistentContext`. The UA no longer
535+
carries a `GStackBrowser` suffix — that branding string was itself a
536+
high-entropy tell, so the browser now reports a plain `Chrome/<version>` UA.
537+
Deepest-layer CDP-protocol detection still gets through (Google can still
538+
trigger captchas; see the CDP-patch item in `TODOS.md`). Your regular Chrome
539+
with your tabs and bookmarks stays untouched.
534540

535541
### When to use headed mode
536542

@@ -581,13 +587,42 @@ A running daemon with config A meeting a new invocation with config B exits
581587
1 with a `browse disconnect` hint instead of silently restarting and dropping
582588
tab state, cookies, or sessions.
583589

584-
**Stealth scope.** When `--headed` or `--proxy` are set, `$B` masks
585-
`navigator.webdriver` only — via Chromium's
586-
`--disable-blink-features=AutomationControlled` plus a small init script.
587-
We do NOT fake `navigator.plugins`, `navigator.languages`, or `window.chrome`
588-
— modern fingerprinters check those for consistency, and synthesizing fixed
589-
values can flag MORE bot-like, not less. ChromeDriver's `cdc_` runtime
590-
artifacts and the Permissions API patch are still cleaned up.
590+
**Stealth scope (Layer C, always on).** Every context — headless `launch`,
591+
`--headed`/`--proxy`, `handoff`, and the `useragent`/`viewport --scale`
592+
rebuild (`recreateContext`) — gets the full Layer C mask, no opt-in flag.
593+
Layer C masks `navigator.webdriver`, restores the `window.chrome.*` shape
594+
(`runtime`, `app`, `csi`, `loadTimes`), aligns `Notification.permission`
595+
with the Permissions API, reports a per-install
596+
`hardwareConcurrency`/`deviceMemory` from the host profile, sweeps the known
597+
Selenium/Phantom/Nightmare/Playwright globals, and installs a
598+
`Function.prototype.toString` proxy so every patched getter reports
599+
`[native code]` even under the depth-3 recursion check. It still does NOT
600+
fake `navigator.plugins` or `navigator.languages` — modern fingerprinters
601+
cross-check those for consistency, and synthesizing fixed values flags MORE
602+
bot-like, not less. ChromeDriver's `cdc_`/`__webdriver` runtime artifacts and
603+
the Permissions notifications tell are also cleaned up on every path.
604+
605+
`GSTACK_STEALTH=extended` (also accepts `1` or `true`; off by default) layers
606+
six more aggressive patches on top — WebGL renderer spoof, a faked
607+
`navigator.plugins` PluginArray, `navigator.mediaDevices`. That mode actively
608+
lies and can break sites that reflect on those properties; use it only when
609+
the default triggers detection. For gbrowser builds with the C++ patches, the
610+
`GSTACK_*` host-profile env (GPU vendor/renderer, UA-CH platform/model,
611+
hardware) emits the Pack 1 `--gstack-gpu-vendor` / `--gstack-gpu-renderer` /
612+
`--gstack-ua-platform` / `--gstack-ua-model` / `--gstack-hw-concurrency` /
613+
`--gstack-device-memory` switches that push the GPU/UA-CH/hardware spoof down
614+
to native code, and `GSTACK_CDP_STEALTH=on` (or `1`/`true`) emits the Pack 2
615+
`--gstack-suppress-prepare-stack-trace` switch (closes the Cloudflare
616+
`Error.prepareStackTrace` canary). On stock Playwright Chromium every one of
617+
these switches is a safe no-op.
618+
619+
`launchHeaded` / `handoff` also strip Playwright's automation-tell launch
620+
defaults via `ignoreDefaultArgs` (`STEALTH_IGNORE_DEFAULT_ARGS`):
621+
`--enable-automation` (the "Chrome is being controlled by automated test
622+
software" infobar), `--disable-extensions`,
623+
`--disable-component-extensions-with-background-pages`,
624+
`--disable-popup-blocking`, `--disable-component-update`, and
625+
`--disable-default-apps`.
591626

592627
**Container support.** `--headed` on Linux without `DISPLAY` walks the
593628
display range (`:99`, `:100`, ...) until `xdpyinfo` reports a free slot,
@@ -1164,6 +1199,11 @@ the global `~/.gstack/browser-skills/foo/` only inside project-a.
11641199
| `GSTACK_BROWSE_MAX_HTML_BYTES` | 52428800 (50MB) | `load-html` size cap |
11651200
| `GSTACK_SECURITY_OFF` | unset | Emergency kill switch — disable ML classifier |
11661201
| `GSTACK_SECURITY_ENSEMBLE` | unset | Set to `deberta` for 3-classifier ensemble (721MB download) |
1202+
| `GSTACK_STEALTH` | unset | Set to `extended` (also accepts `1`/`true`) to layer six aggressive patches (WebGL spoof, faked plugins, mediaDevices) on top of Layer C. Actively lies; can break sites. |
1203+
| `GSTACK_CDP_STEALTH` | unset | Set to `on`/`1`/`true` to emit `--gstack-suppress-prepare-stack-trace` (gbrowser Pack 2 / B11 C++ patch only; no-op on stock Chromium) |
1204+
| `GSTACK_GPU_VENDOR`, `GSTACK_GPU_RENDERER`, `GSTACK_GPU_CHIPSET` | unset | Per-install GPU spoof fed to the Pack 1 WebGL/UA-CH C++ patches. Set by gbd from the host profile; emitted as `--gstack-gpu-vendor` / `--gstack-gpu-renderer` / `--gstack-ua-model` cmdline switches only when present. |
1205+
| `GSTACK_PLATFORM` | unset | Host platform classification (`MacARM`/`MacIntel``macOS`, `Win32``Windows`, `Linux*``Linux`) emitted as `--gstack-ua-platform` |
1206+
| `GSTACK_HW_CONCURRENCY`, `GSTACK_DEVICE_MEMORY` | host profile (fallback 8) | Per-install `hardwareConcurrency`/`deviceMemory` reported by Layer C and emitted as `--gstack-hw-concurrency` / `--gstack-device-memory` for the worker-navigator C++ patch |
11671207

11681208
---
11691209

@@ -1179,7 +1219,7 @@ browse/
11791219
│ ├── proxy-config.ts # --proxy URL parsing + cred resolution (URL vs env, fail-fast on both)
11801220
│ ├── proxy-redact.ts # Cred-redaction helper for any proxy URL surfaced to logs/errors
11811221
│ ├── xvfb.ts # Xvfb auto-spawn + orphan cleanup with PID + start-time validation
1182-
│ ├── stealth.ts # navigator.webdriver mask + cdc_ cleanup + Permissions API patch
1222+
│ ├── stealth.ts # Layer C: webdriver mask + window.chrome.* + Notification/Permissions + per-install hardware + toString proxy + automation-global sweep; buildGStackLaunchArgs (GSTACK_* cmdline switches); GSTACK_STEALTH=extended opt-in
11831223
│ ├── browse-client.ts # Canonical SDK — what skills import as _lib/browse-client.ts
11841224
│ ├── snapshot.ts # AX tree → @e/@c refs → Locator map; -D/-a/-C handling
11851225
│ ├── read-commands.ts # Non-mutating: text, html, links, js, css, is, dialog, ...

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
# Changelog
22

3+
## [1.58.3.0] - 2026-06-18
4+
5+
## **GBrowser masks the full set of automation tells by default, on every path a page can reach.**
6+
## **Layer C stealth is always on, carries a per-install hardware identity, and survives the toString depth-3 trick.**
7+
8+
GBrowser's headless and headed Chromium now ship "Layer C" anti-detection by default, with no opt-in flag. Where the old default masked only `navigator.webdriver`, the browser now also restores the full `window.chrome.*` shape (runtime, app, csi, loadTimes), aligns `Notification.permission` with the Permissions API, reports a per-install `hardwareConcurrency`/`deviceMemory` from the host profile, sweeps the known Selenium/Phantom/Nightmare/Playwright globals, and installs a `Function.prototype.toString` proxy so every patched getter reports `[native code]` even under the depth-3 recursion check. The aggressive `GSTACK_STEALTH=extended` mode (WebGL spoof, faked plugins, mediaDevices) still exists, now layered on top of Layer C rather than replacing it. And stealth applies on all four context-creation paths, so a `useragent` change, a `viewport --scale`, or a headless-to-headed handoff hands a site a fully masked page every time.
9+
10+
### The numbers that matter
11+
12+
Source: `bun test browse/test/stealth-layer-c.test.ts browse/test/stealth-webdriver.test.ts browse/test/stealth-extended.test.ts browse/test/browser-manager-unit.test.ts` (80 tests, real Chromium for the runtime checks).
13+
14+
| Capability | Before (v1.58.1.0) | After (v1.58.3.0) |
15+
|---|---|---|
16+
| Automation tells masked by default | 1 (navigator.webdriver) | 7 categories (webdriver, window.chrome.*, Notification, per-install hardware, toString-native, automation-global sweep, cdc/Permissions) |
17+
| Context paths that apply stealth | 2 (launch, launchHeaded) | 4 (+ handoff, + recreateContext) |
18+
| toString integrity | not addressed | survives the depth-3 `[native code]` check |
19+
| Hardware identity | generic Chromium default | per-install, from the host profile |
20+
| Stealth tests | none dedicated | 80 passing (incl. real-Chromium runtime) |
21+
22+
By default the browser now masks seven categories of automation tell instead of one, on every path a page can reach, not just the first launch.
23+
24+
### What this means for builders
25+
26+
If you drive GBrowser to dogfood, scrape, or QA against anti-bot-protected targets, your sessions look like a real per-install Chrome out of the box. There is no `GSTACK_STEALTH` flag to remember, and no silent gap where a routine `useragent` or `viewport --scale` strips the mask. For gbrowser builds with the Pack 1 C++ patches, set the `GSTACK_*` host-profile env (gbd does this) to push the GPU/UA-CH/hardware spoof down to native code; on stock Playwright Chromium the same call is a safe no-op.
27+
28+
### Itemized changes
29+
30+
#### Added
31+
- Always-on Layer C stealth (`buildStealthScript`): webdriver mask, `window.chrome.{runtime,app,csi,loadTimes}` shape, `Notification.permission` alignment, per-install `hardwareConcurrency`/`deviceMemory`, a `Function.prototype.toString` proxy that holds up under the depth-3 `[native code]` check, and a static sweep of Selenium/Phantom/Nightmare/Playwright globals.
32+
- `buildGStackLaunchArgs`: per-install `--gstack-*` cmdline switches (GPU vendor/renderer, UA-CH platform/model, hardware concurrency/memory) for gbrowser's Pack 1 C++ patches, emitted only when the matching `GSTACK_*` env is set so stock Chromium is unaffected.
33+
- Real-Chromium runtime coverage: webdriver, chrome.* shape, Notification/Permissions pairing, toString depth-3, per-install hardware, and the extended-mode blend (80 stealth tests).
34+
35+
#### Changed
36+
- Stealth applies on every context-creation path (`launch`, `launchHeaded`, `handoff`, `recreateContext`), so a `useragent`, `viewport --scale`, or handoff keeps the full mask.
37+
- The cdc_/`__webdriver` cleanup and the Permissions notifications shim live in `applyStealth`, so headless and handoff get the same `Notification.permission`/`permissions.query` consistency as the headed path.
38+
- `GSTACK_STEALTH=extended` layers on top of Layer C; the always-on default does not fake `navigator.plugins` (the opt-in mode still does, as the documented "may break sites" escape hatch).
39+
- `--gstack-suppress-prepare-stack-trace` is opt-in via `GSTACK_CDP_STEALTH=on`, so the switch never reaches a Chromium that does not understand it.
40+
- `--disable-blink-features=AutomationControlled` comes from one shared `STEALTH_LAUNCH_ARGS` constant across every launch path.
41+
342
## [1.58.1.0] - 2026-06-14
443

544
## **Local evals stop lying. Spawned `claude` test children run in a sealed clean room,**

TODOS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1994,7 +1994,7 @@ Shipped in v0.6.5. TemplateContext in gen-skill-docs.ts bakes skill name into pr
19941994

19951995
**What:** Write a postinstall script that patches Playwright's CDP layer to suppress `Runtime.enable` and use `addBinding` for context ID discovery, same approach as rebrowser-patches. Eliminates the `navigator.webdriver`, `cdc_` markers, and other CDP artifacts that sites like Google use to detect automation.
19961996

1997-
**Why:** Our current stealth narrows to `navigator.webdriver` masking + ChromeDriver `cdc_` runtime cleanup + Permissions API patch (v1.28.0.0 narrowed it from also faking plugins/languages, since modern fingerprinters punish inconsistent fakes more than they punish admitted defaults). That's enough for most sites but Google still triggers captchas, because the real detection is at the CDP protocol level. rebrowser-patches proved the approach works but their patches target Playwright 1.52.0 and don't apply to our 1.58.2. We need our own patcher using string matching instead of line-number diffs. 6 files, ~200 lines of patches total.
1997+
**Why:** As of v1.58.3.0 our JS-layer stealth is "Layer C" — always-on `navigator.webdriver` mask + `window.chrome.*` shape + `Notification.permission`/Permissions alignment + per-install `hardwareConcurrency`/`deviceMemory` + a `Function.prototype.toString` proxy + an automation-global sweep + ChromeDriver `cdc_`/`__webdriver` cleanup (still NOT faking plugins/languages, since modern fingerprinters punish inconsistent fakes more than they punish admitted defaults). That closes most JS-observable tells, but Google still triggers captchas because the deepest detection is at the CDP protocol level, which a page-world init script can't reach. rebrowser-patches proved the CDP approach works but their patches target Playwright 1.52.0 and don't apply to our 1.58.2. We need our own patcher using string matching instead of line-number diffs. 6 files, ~200 lines of patches total. (Layer C's toString proxy still has descriptor/Reflect.ownKeys surfaces; pushing the spoofs to native code via CDP suppression or the Chromium fork makes the JS layer obsolete.)
19981998

19991999
**Context:** Full analysis of rebrowser-patches source: patches 6 files in `playwright-core/lib/server/` (crConnection.js, crDevTools.js, crPage.js, crServiceWorker.js, frames.js, page.js). Key technique: suppress `Runtime.enable` (the main CDP detection vector), use `Runtime.addBinding` + `CustomEvent` trick to discover execution context IDs without it. Our extension communicates via Chrome extension APIs, not CDP Runtime, so it should be unaffected. Write E2E tests that verify: (1) extension still loads and connects, (2) Google.com loads without captcha, (3) sidebar chat still works.
20002000

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.58.1.0
1+
1.58.3.0

0 commit comments

Comments
 (0)