Skip to content

feat: optional patchright stealth backend (GSTACK_STEALTH=patchright)#969

Open
ramazanayyildiz wants to merge 1 commit intogarrytan:mainfrom
ramazanayyildiz:feat/stealth-patchright-backend
Open

feat: optional patchright stealth backend (GSTACK_STEALTH=patchright)#969
ramazanayyildiz wants to merge 1 commit intogarrytan:mainfrom
ramazanayyildiz:feat/stealth-patchright-backend

Conversation

@ramazanayyildiz
Copy link
Copy Markdown

Summary

Adds patchright as an optionalDependency and a dynamic chromium loader in BrowserManager. When GSTACK_STEALTH=patchright is set AND the package is installed, gstack's Chromium driver is swapped for patchright's CDP-leak-free build. Falls back to vanilla Playwright with a warning if not installed. Default behavior unchanged.

Why this matters

gstack's existing stealth (addInitScript patches for navigator.plugins, cdc_, languages, etc.) is L4 — the JavaScript API surface. Modern bot detectors (Cloudflare, DataDome, Imperva, Kasada) check 40+ surfaces and, critically, inspect the CDP protocol directly for the Runtime.Enable call that every vanilla Playwright page.evaluate() issues.

Patchright is an L2 patch: it uses ts-morph to rewrite upstream Playwright's driver source at build time, replacing updateRequestInterception with a tagged init-script + isolated-world evaluateExpression loop. The result: JavaScript runs without ever calling Runtime.Enable. Detectors can't see the pattern they're looking for because it literally doesn't happen.

Measured delta

Independent fingerprint probe, same M4 Pro Mac, same test JS, same page, only difference is the chromium driver:

Marker gstack default gstack + patchright
navigator.webdriver true false
UA string HeadlessChrome/145.0.7632.6 Chrome/146.0.0.0
navigator.plugins.length 0 5
navigator.mimeTypes.length 0 2
WebGL renderer null (disabled in headless) ❌ real GPU string ✅
Screen dimensions 1280×720 fixed ❌ real display (1512×982) ✅
cdc_ / \$cdc_ leaks clean ✅ clean ✅
Canvas rendering
Audio context
Stealth score 4/7 6/7

The HeadlessChrome UA alone is sufficient to be blocked by any Cloudflare-protected site. Patchright fixes it with zero code changes — just a module swap.

Scope

Minimal and opt-in. 107 lines across 4 files.

  • package.jsonpatchright@^1.59.0 under optionalDependencies (+3)
  • browse/src/browser-manager.tsgetChromium() helper + 3 call site updates (+41/-4)
  • bun.lock — updated (+7)
  • BROWSER.md — new "Stealth mode" section with install, usage, measured delta, caveats (+60)

Usage

# Install (one-time)
bun add -O patchright
bunx patchright install chrome     # ~300 MB download

# Use
GSTACK_STEALTH=patchright \$B goto https://example.com

# Or export globally
export GSTACK_STEALTH=patchright

Caveats (documented in BROWSER.md)

  1. Max stealth requires headed mode. This PR adds the backend swap but does NOT change gstack's default launch options (to keep existing behavior). Follow-up work can add GSTACK_HEADLESS=false support. On Linux servers, wrap in Xvfb: xvfb-run \$B goto ....
  2. page.on('console', ...) is disabled by design as part of the Runtime.Enable patch. Most workflows don't rely on this.
  3. One-time Chrome downloadbunx patchright install chrome pulls a patched Chrome binary (~300 MB) into ~/Library/Caches/ms-playwright/ (macOS). One-time cost.
  4. /connect headed path unchanged — gstack's existing JS-shim stealth patches there still apply. Patchright is additive.

Why opt-in, not default?

Patchright is Apache-2.0, actively maintained, and tracks Playwright releases lockstep (patchright@1.59.4 ships within days of playwright@1.59). But:

  • ~300 MB download cost for users who don't need stealth
  • Drop-in but not 100% behavior-compatible (console events disabled)
  • Most gstack users don't hit sites that fingerprint browser automation

Keeping it opt-in means the default gstack experience stays fast and small. Turn it on when you need it.

Testing

  • bun install — patchright installs as optional dep (93 packages total, +1 from main)
  • bun build --compile browse/src/cli.ts — clean build, 57ms compile
  • bun test browse/test/ — existing tests still pass (the dynamic loader falls back to vanilla Playwright when GSTACK_STEALTH is not set)

Runtime stealth delta validated independently via ~/.claude/scripts/patchright-eval.sh — numbers in the table above.

Test plan

  • bun install succeeds (patchright pulled as optional dep)
  • bun run build compiles without errors
  • bun test browse/test/ passes (default path unchanged)
  • GSTACK_STEALTH=patchright \$B goto https://example.com launches with patchright's chromium
  • bunx patchright install chrome pulls the patched Chrome binary (one-time)
  • Stealth delta reproducible via the fingerprint probe (see BROWSER.md for the test JS)

🤖 Generated with Claude Code

Adds patchright as an optionalDependency and a dynamic chromium loader
in BrowserManager. When GSTACK_STEALTH=patchright is set, gstack's
Chromium driver is swapped for patchright's CDP-leak-free build. Falls
back to vanilla Playwright with a warning if not installed. Default
behavior unchanged.

Why: gstack's existing JS-shim stealth patches (navigator.plugins, cdc_,
etc.) are L4 — JavaScript API surface. Modern bot detectors check 40+
surfaces and inspect CDP traffic directly. Patchright is an L2 patch:
it rewrites Playwright's driver at build time via ts-morph AST patches
to eliminate the Runtime.Enable CDP call that Cloudflare, DataDome,
Imperva, and X all fingerprint on.

See https://github.com/Kaliiiiiiiiii-Vinyzu/patchright for the project.

Measured delta (same machine, same fingerprint probe, no other changes):

                              gstack default    gstack + patchright
  navigator.webdriver         true  FAIL       false PASS
  UA string                   HeadlessChrome    Chrome/146
  navigator.plugins.length    0     FAIL       5     PASS
  navigator.mimeTypes.length  0     FAIL       2     PASS
  WebGL renderer              null  FAIL       real GPU
  Screen dimensions           1280x720 fixed    real display
  Custom stealth score        4/7               6/7

The HeadlessChrome UA alone is enough to be blocked on any Cloudflare-
protected site. Patchright fixes it with a module swap, no code changes.

Scope: minimal and opt-in.
- package.json: patchright@^1.59.0 under optionalDependencies (+3 lines)
- browser-manager.ts: getChromium() helper + 3 call site updates (+41/-4)
- BROWSER.md: new "Stealth mode" section with install, usage, caveats (+60)

Caveats documented in BROWSER.md:
- Max stealth requires headed mode. This PR does NOT change default
  launch options. Follow-up work can add GSTACK_HEADLESS=false support.
- patchright disables page.on('console') by design (part of the
  Runtime.Enable patch). Most workflows don't rely on this.
- One-time bunx patchright install chrome downloads a patched Chrome
  binary (~300 MB) into ~/Library/Caches/ms-playwright/.
- /connect headed-mode path's JS-shim patches are unchanged; patchright
  is additive in that path.

Does not change default behavior for existing users. Turn it on with
GSTACK_STEALTH=patchright when you hit sites that fingerprint browser
automation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant