Skip to content

About

Free, self-hosted alternative to Browserbase-style hosted browsers: escalates curl_cffi → Patchright → nodriver (→ optional proxy / CAPTCHA solver) only when a page is actually blocked. Block detection, shared logged-in Chrome profile, benchmark.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

StealthStack

A free, self-hosted alternative to paid hosted-browser APIs (Browserbase, Browserless, Steel, Hyperbrowser…) for fetching pages behind bot protection. It tries the cheapest method first and only escalates when a page is actually blocked.

 curl_cffi ──blocked?──▶ patchright ──blocked?──▶ nodriver ──blocked?──▶ + residential proxy ──▶ + CAPTCHA solver
   ~1 s                    ~6 s                     ~7 s                  optional, paid           optional, paid
   no browser              real Chrome, headed      raw CDP, no webdriver
  • Free by default. The three free tiers use open-source tools. The paid tiers stay off until you add a key.
  • Cheap first. A page that works over plain HTTP never launches a browser.
  • Knows when it's blocked. It detects Cloudflare, DataDome and PerimeterX challenges, error codes, Chrome network errors and JS-only shells.
  • Logged-in sessions. One shared persistent Chrome profile, so you log in once by hand and every browser tier uses it.
  • One command. python fetch.py <url> returns JSON plus the page text, ready for scripts or AI agents.

Why this exists

Hosted browser services are useful, but most of what they charge for breaks down into three parts:

  1. A browser that doesn't look automated. Free tools now cover this: Patchright and nodriver.
  2. Residential IPs. A lot of blocking comes down to IP reputation. You can buy residential proxies directly, often for less per GB than the bundled price.
  3. CAPTCHA solving. Pay-per-solve APIs cost about $1–3 per 1,000 solves.

StealthStack wires the free part into one escalation ladder with block detection, so you only pay for proxies or solvers on the pages that need them, if any. Hosted services are still the better choice when you need hundreds of concurrent browsers, live session view or human takeover for customers, or have nobody to look after the setup.

Install

git clone https://github.com/Sayandeep1013/stealthstack
cd stealthstack
python -m venv .venv
.venv/Scripts/pip install -r requirements.txt        # Linux/macOS: .venv/bin/pip install -r requirements.txt

You need Python 3.10+ and a Chrome or Chromium browser:

  • Installed Google Chrome is detected automatically.
  • If you don't have it, run .venv/Scripts/patchright install chromium.
  • Or point CHROME_PATH in .env at any Chrome or Chromium build.

Optionally, cp .env.example .env to change settings (see Configuration).

Usage

Command line

python fetch.py https://example.com --text             # auto-escalate; JSON summary + visible page text
python fetch.py <url> --only nodriver --screenshot    # force a single tier, save a PNG
python fetch.py <url> --start patchright              # skip the cheap tier for sites you know need a browser
python fetch.py <url> --show                          # keep the browser window on-screen (debugging)
Flag Meaning
--start TIER Start the ladder at this tier (default curl)
--only TIER Run exactly one tier
--show Show the browser window instead of parking it off-screen
--screenshot Save a screenshot of the final page to out/
--text Also print the page's visible text (capped at 20k chars)

The tiers are curl, patchright, nodriver, curl+proxy and patchright+proxy. Exit code 0 means success; 1 means every tier was blocked.

Example output:

{
  "url": "https://www.example-protected-site.com/page",
  "ok": true,
  "tier": "patchright",
  "status": 200,
  "title": "The real page title",
  "html_file": "out/www.example-protected-site.com_20261022-114115.html",
  "screenshot": "",
  "attempts": [
    {"tier": "curl", "ok": false, "status": 403, "ms": 190, "reason": "cloudflare challenge header"},
    {"tier": "patchright", "ok": true, "status": 200, "ms": 3071, "reason": ""}
  ]
}

Python

from stealth import fetch, run_tier

final, attempts = fetch("https://example.com")            # full ladder
print(final.ok, final.tier, final.title)
html, text = final.html, final.text

r = run_tier("nodriver", "https://example.com", screenshot=True)   # one tier

Result fields: url, tier, ok, status, title, reason, ms, html, text, screenshot.

With AI agents

The JSON output plus --text is designed for coding agents such as Claude Code or Cursor. Tell the agent to run python fetch.py <url> --text whenever its normal web fetch gets blocked. Because the exit code signals success or failure, the agent can branch on it.

How it works

The tiers

Tier Tool What it fixes What it doesn't fix
curl curl_cffi with impersonate="chrome" Python's TLS/JA3/HTTP2 fingerprint, which many WAFs block on sight JavaScript challenges and JS-rendered pages
patchright Patched Playwright driving real Chrome The CDP Runtime.enable leak and the navigator.webdriver flags. Runs headed (headless is detected far more often), with the window parked off-screen Bad IP reputation, interactive CAPTCHAs
nodriver Raw CDP, no webdriver layer A different automation footprint, so it passes some sites that flag patchright Same as above. Can't use authenticated proxies
curl+proxy Tier 1 through PROXY_URL IP-reputation blocks on simple pages JS challenges
patchright+proxy Tier 2 through PROXY_URL IP-reputation blocks on JS-protected pages. Images, fonts and media are blocked to save proxy GB Hard interactive CAPTCHAs (use the solver)

Block detection

stealth/ladder.py: block_reason() decides whether a response counts as blocked. In order, it checks:

  1. A cf-mitigated: challenge response header
  2. Challenge page titles: "Just a moment", "Access denied", "Attention required", "Pardon our interruption"…
  3. On short pages only: challenge text and vendor markers (DataDome captcha-delivery.com, PerimeterX px-captcha, "Press & hold", "unusual traffic"…). Long real pages that happen to mention these words don't trigger it.
  4. Chrome network-error pages (ERR_CONNECTION_RESET…)
  5. HTTP 401/403/407/429/503 and any other 4xx/5xx
  6. "Thin" pages: under 200 characters of text plus several scripts, meaning the page needs a browser to render

Browser tiers poll the page for up to 15 seconds while a challenge resolves itself. Once per page, they also try clicking a Cloudflare "Verify you are human" checkbox.

CAPTCHA solving (optional)

With CAPSOLVER_API_KEY set, browser tiers look for embedded Cloudflare Turnstile and reCAPTCHA v2 widgets. They send them to CapSolver, inject the returned token and fire the widget's callback (stealth/captcha.py). Full-page Cloudflare managed challenges can't be solved by token injection; use a residential proxy for those. Experimental: it isn't covered by the benchmark.

Logged-in sessions

Patchright and nodriver share one persistent Chrome profile (profiles/chrome), so one login covers both tiers.

Option A: log in directly. Run python login.py [url] (or double-click login.bat on Windows). The profile opens on-screen; log in to whatever you need, then close the window.

Option B: reuse a profile you already log into, such as the one your Playwright MCP server uses. Set MASTER_PROFILE in .env. login.py then opens that profile, and sync_logins.py clones it into profiles/chrome (skipping caches, so it takes about 10 MB) when you close the window. This works because it's the same Chrome build under the same OS user, which can decrypt the copied cookies.

python check_logins.py reports whether Google, GitHub, LinkedIn and X are logged in.

Rules that keep your accounts safe:

  • One browser per profile at a time. Don't run fetches in parallel, and don't sync while the master profile is open. sync_logins.py refuses if either profile is in use.
  • The proxy tiers never use the logged-in profile. They get their own empty profiles/chrome-proxy, because a login suddenly coming from a new residential IP is a classic trigger for account lockouts.
  • profiles/ holds live session cookies. It's gitignored. Never commit or share it.

Configuration

Every setting is optional. Put them in .env (copy .env.example) or set them as environment variables.

Variable Default Purpose
CHROME_PATH System Chrome if found, else the tool's default Chrome/Chromium binary to drive
PLAYWRIGHT_BROWSERS_PATH Playwright default Where patchright install keeps its browsers
MASTER_PROFILE unset Existing profile to clone logins from
CHROME_PROFILE profiles/chrome Shared logged-in profile
STEALTH_TMP tmp/ Scratch files. TEMP/TMP point here, so nothing lands in the system temp folder
STEALTH_OUT out/ Saved HTML, screenshots and benchmark reports
PROXY_URL unset (tiers off) http://user:pass@host:port, a residential proxy is recommended
CAPSOLVER_API_KEY unset (solver off) CapSolver key

Benchmark

python bench.py                      # every configured tier on every target
python bench.py curl nodriver        # just these tiers

It writes out/bench_<time>.json. Add your own sites to targets.local.txt (gitignored, one name url per line).

Results from one run on a home connection with no proxy, October 2026. They vary with your IP and over time.

Public test pages:

Target curl patchright nodriver
control (example.com) pass pass pass
nopecha Cloudflare managed challenge block block block
nopecha Turnstile demo block block block
antoinevastel DataDome test pass pass pass
BrowserScan bot detection pass pass pass

The public demo pages don't discriminate well. Most either load for everyone or force an interactive challenge on everyone (that one needs a residential IP or a solver). Real sites are the better test.

Five well-known real sites protected by Cloudflare, DataDome and similar (names withheld):

curl patchright nodriver
Passed 2/5 4/5 4/5
Typical time ~0.2–1.3 s ~3–8 s ~6–7 s

The remaining site reset the connection at the network level, which is a job for the proxy tier. In earlier runs nodriver occasionally got through it.

Project layout

fetch.py            CLI: run the ladder on one URL
bench.py            Every tier × every target, pass/fail report
login.py / .bat     Open the login browser; sync afterwards if MASTER_PROFILE is set
sync_logins.py      Clone MASTER_PROFILE -> profiles/chrome
check_logins.py     Which common sites is the shared profile logged into
stealth/
  config.py         .env loading, paths, Chrome discovery, TEMP redirection
  ladder.py         Tiers, block detection, escalation logic
  captcha.py        Optional CapSolver integration

Troubleshooting

Symptom Fix
No Chrome found / patchright can't launch Install Chrome, run patchright install chromium, or set CHROME_PATH
"Profile in use" / launch hangs A browser is still open on that profile. Close it, or check for leftover chrome.exe processes
A real page reported as blocked (or the reverse) Adjust TITLE_SIGNS / BODY_SIGNS / thresholds in block_reason()
Everything blocked on a VPS Datacenter IPs are blocked much more than home IPs. Set PROXY_URL
Headed Chrome fails on a Linux server Run under xvfb-run python fetch.py …
Logins didn't carry over after sync_logins.py The master profile must use the same Chrome build and the same OS user

Limits

  • It beats basic and mid-level bot protection. It doesn't guarantee getting past Akamai, Kasada or heavily tuned DataDome.
  • nodriver can't take authenticated (user:pass@) proxies, so the proxy tiers use curl and patchright.
  • It's built for sequential fetching on one machine. For hundreds of concurrent browsers you need a pool (or a hosted service).
  • It was developed and tested on Windows 11. The code is cross-platform, but Linux and macOS are less tested.

Responsible use

This is for automating access to pages you're allowed to access: your own accounts, public data, and testing your own sites. Respect each site's terms of service and robots.txt, rate-limit your requests, and don't use it for credential stuffing, account abuse, or scraping personal data. You are responsible for how you use it.

Credits

Built on curl_cffi, Patchright and nodriver. All the hard anti-detection work is theirs.

License

MIT

About

Free, self-hosted alternative to Browserbase-style hosted browsers: escalates curl_cffi → Patchright → nodriver (→ optional proxy / CAPTCHA solver) only when a page is actually blocked. Block detection, shared logged-in Chrome profile, benchmark.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages