Stop getting blocked — without puppeteer-stealth. JS stealth patches self-reveal: the page checks whether the getter is native code and catches the override. Fortress compiles the fingerprint correction into Chromium's C++, so a page inspecting itself sees stock Chrome. Drop it in under the Playwright or Puppeteer you already use.
Blink · V8 · BoringSSL patched in-tree · ANGLE / D3D11-backed WebGL · JA3/JA4-coherent TLS · monthly upstream rebase · reproducible, gauntlet-gated releases
| single-surface C++ patches |
CreepJS headless / stealth |
across every realm |
open engine, rebuild it yourself |
Unedited capture of the Fortress binary in a real window: it clears a live Cloudflare challenge, turns bot.sannysoft.com all green, then reads BrowserScan “Normal”. Reproduce with tools/gauntlet.py.
|
Every spoofed getter is a C++ getter — |
nodriver-style raw CDP on |
0% headless on CreepJS; Sannysoft, BrowserScan, and live Cloudflare Turnstile cleared — as a stock Chrome install. |
|
34 small single-purpose diffs in |
Real V8, Blink, and BoringSSL keep engine, user-agent, and JA3/JA4 TLS shape in agreement — a Windows persona on a matching stack. |
One binary, a coherence-checked Windows identity; |
| What it is · Quick start | the pitch, install, first script, AI-agent setup |
| Why an engine fork wins | the self-revealing-JS thesis + the three detection layers |
| How Fortress compares | vs puppeteer-stealth · Camoufox · CloakBrowser · closed vendors |
| Proof — live-detector results | CreepJS / Sannysoft / BrowserScan / Cloudflare, with screenshots |
| Configure the persona | the --uxr-* fingerprint surface |
| Works with your stack | browser-use · Crawl4AI · Stagehand · LangChain |
| Build & verify | reproduce from source, verify provenance |
| Reference | troubleshooting · FAQ · roadmap · repo layout |
Fortress is a Chromium fork that corrects the browser fingerprint surfaces bot detectors read (canvas, WebGL, audio, fonts, navigator, and about thirty more) in the engine's C++. It ships as an ordinary binary with a CDP endpoint, so you keep Playwright, Puppeteer, or any CDP client and point it at Fortress. Swap the browser, keep your code.
JavaScript stealth has a ceiling. Tools like puppeteer-stealth overwrite properties after the page loads, but a detector checks whether the function returning a value is native code. An injected override gives itself away: .toString() shows its source, and re-grabbing the same primitive from an iframe or worker reaches a realm past the patch's reach. The extra layer is the tell.
Fortress moves the correction into the engine, so navigator.vendor resolves to the real C++ getter. It reports [native code] and reads the same across the main frame, iframes, and workers. A page inspecting itself sees stock Chromium. In practice your scraper or agent gets blocked less — it clears CreepJS, Sannysoft, BrowserScan, and live Cloudflare Turnstile as a normal Chrome install, and whatever blocking is left traces to your proxies and behavior.
from tilion_fortress import Fortress
from playwright.sync_api import sync_playwright
with Fortress() as f: # launches the stealth engine on a CDP endpoint
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(f.cdp_url)
page = browser.new_page()
page.goto("https://bot.sannysoft.com")
page.screenshot(path="all-green.png")import { Fortress } from "tilion-fortress";
import { chromium } from "playwright";
const f = await Fortress.launch(); // stealth engine on a CDP endpoint
const browser = await chromium.connectOverCDP(f.cdpUrl);
const page = await browser.newPage();
await page.goto("https://browserscan.net");
await browser.close();
await f.close();Unedited captures of the Fortress engine driven over CDP. No stealth plugins, no JS patches — the fingerprint is corrected in the binary. Reproduce any of these with examples/scrape_demos.py.
Structured extraction — records build into typed JSON as each item is read.
![]() Auto-pagination — 30 quotes across 3 pages. |
![]() Deep detail crawl — UPC · price · tax · stock · reviews. |
# Python / Node — prebuilt native binary auto-fetched (Linux x64 & Windows x64), SHA-256 verified
pip install tilion-fortress
npm install tilion-fortress
# Any OS via Docker — raw CDP on :9222 (~302 MB pull / 851 MB on disk, stripped single-layer)
docker run --rm -p 9222:9222 tilion/fortress:latest
# Portable bundle (extract-and-run, like a Chromium snapshot)
tar xzf tilion-fortress-linux-x64.tar.gz # Linux
./tilion-fortress/tilion --headless=new --remote-debugging-port=9222 --user-data-dir=/tmp/p
# Debian / Ubuntu
sudo apt install ./tilion-fortress_151.0.7908.0_amd64.deb && tilion https://example.comTip
The SDK ships the compiled build plus patches/, so you can rebuild the engine yourself and verify every surface correction against the source. Downloads are SHA-256-verified against the release SHA256SUMS automatically.
Fortress is the browser for your agent — raw CDP on :9222, no stealth plugins to wire up. Two ways in.
Option 1 — open it pre-loaded in a chat assistant. One click; it reads our AGENTS.md and walks you through the whole setup:
Option 2 — Copy for agent (everything, to your clipboard). Hit the copy icon at the top-right of the box below — it puts the entire setup context (what it is, install, connect, persona, rules — all of AGENTS.md condensed) on your clipboard. Paste it into Cursor, Claude Code, Copilot, ChatGPT, or any agent and it takes it from there:
You're setting up Fortress, an open-source STEALTH Chromium engine, for browser automation.
It corrects the browser fingerprint (canvas, WebGL, audio, fonts, navigator, +30 more) in Chromium's
C++ and exposes raw CDP on http://localhost:9222 — a drop-in for Playwright/Puppeteer. Do NOT add
puppeteer-stealth or any JS fingerprint patching (it self-reveals and undoes Fortress).
LAUNCH (pick one; all expose CDP on http://localhost:9222):
Docker: docker run -d --rm -p 9222:9222 tilion/fortress:latest
Python: pip install tilion-fortress then from tilion_fortress import Fortress; f=Fortress(); f.start()
Node: npm install tilion-fortress then import {Fortress} from "tilion-fortress"; const f=await Fortress.launch()
CONNECT (keep my existing automation code):
Playwright(py): browser = p.chromium.connect_over_cdp("http://localhost:9222")
Playwright(js): const browser = await chromium.connectOverCDP("http://localhost:9222")
Puppeteer(js): const browser = await puppeteer.connect({ browserURL: "http://localhost:9222" })
browser-use / Crawl4AI / Stagehand / LangChain: point their CDP endpoint at http://localhost:9222
PERSONA (optional; default is a coherent Windows identity) — override any surface with --uxr-* flags:
--uxr-timezone=America/New_York --uxr-hw-concurrency=16 --uxr-languages=en-US,en
RULES:
1) Drive over raw CDP (:9222); don't spawn chromedriver.
2) Never pass --user-agent (use --uxr-ua-*) — it desyncs UA vs UA-Client-Hints.
3) No puppeteer-stealth / undetected-chromedriver / JS fingerprint patches.
4) Blocked ~90% = my IP (datacenter), not the fingerprint — use a residential/mobile proxy, then retry.
Now walk me through launching Fortress and wiring my automation to it.
Full guide: https://github.com/tiliondev/fortress/blob/main/AGENTS.md
The usual approach patches navigator.webdriver, spoofs the WebGL vendor, and overrides navigator.plugins from script. CreepJS and similar detectors still flag it — and the reason is structural, not one more property left uncovered. A JavaScript spoof is a function standing where a native one belongs. Detectors set the returned value aside and interrogate whether the thing returning it is native:
-
toStringself-reveal — a native method stringifies tofunction get vendor() { [native code] }; your override stringifies to its own source. One.toString()catches it. -
Descriptor &
hasOwnPropertytells —getOwnPropertyDescriptorexposes redefined props;hasOwnProperty('toString')returnstrueon a tampered function,falseon a native one. -
failsTypeError— native getters throw a specificTypeErroron the wrongthis; a naive shim stays quiet, and the silence is the signal. -
Realm re-acquisition defeats every main-world patch — a detector grabs a pristine primitive from another realm and turns it on your function:
const iframe = document.createElement('iframe'); document.body.appendChild(iframe); const realToString = iframe.contentWindow.Function.prototype.toString; realToString.call(navigator.__lookupGetter__('vendor')); // returns your source code. Caught.
Your main-world patch lives in a different realm from that iframe. The same trap fires from a Web Worker — a thread your main-thread shim runs beside rather than inside.
Fortress has no such layer. The getter for navigator.vendor is the C++ getter — it reports [native code] because it is native code, identical across every realm. Camoufox puts it well: "there is no JavaScript hijacking to be detected." Fortress applies the same idea to V8 and Blink in place of Gecko.
Modern anti-bots (Cloudflare, DataDome, Kasada, HUMAN, Akamai) read three structurally different surfaces, in three separate places. One tool rarely fixes all three:
Fortress is the Layer-C engine, built to be driven so A and B hold too. The binary alone leaves the CDP channel open — that part is on the control layer, and pretending otherwise is how you get caught.
Fortress builds on real prior art — fingerprint-chromium, ChromiumFish, and CloakBrowser came first, and commercial vendors (Multilogin, Kameleo, GoLogin, AdsPower, Browserbase, Surfsky) recompile Chromium behind closed source. Most of that work stays closed: the paywalled forks hand you a binary and ask you to trust it, and the vendors keep their patches in-house.
Fortress goes the other way, because a stealth engine only stays useful when the people relying on it can see how it works. Every surface correction lives in patches/ as a small, single-purpose diff you can read in a minute, and the whole engine rebuilds from source with one script. When a detector finds a new tell, you trace the fix, patch it, and send it back. That feedback loop is the point — and it only works while the engine stays open enough to read, extend, and rebuild.
Reproduce any row with tools/gauntlet.py --bundle ./tilion-fortress. Verified against live detectors; re-run dated in docs/GAUNTLET_RESULTS.md.
| Suite | Stock Chromium | Fortress |
|---|---|---|
| CreepJS | flagged headless | 0% headless · 0% stealth, worker signals coherent |
| bot.sannysoft.com | red rows | 0 failed · WebDriver Advanced passed · WebGL = NVIDIA RTX 3060 / ANGLE D3D11 |
| browserscan.net | bot detected | “No bots detected, could be a human” |
| rebrowser bot-detector | Runtime.enable LEAK |
no leak · webdriver=false · clean init-scripts (raw CDP) |
| Cloudflare Turnstile | blocked | bypassed — a human click cleared a live challenge (headed, datacenter IP) |
The binary carries zero brand strings; the launcher applies a coherent default Windows persona. Override any surface with --uxr-* switches, or set TILION_NO_DEFAULTS=1 for a bare launch.
--uxr-platform / --uxr-ua-platform / --uxr-ua-os / --uxr-ua-arch / --uxr-ua-bitness
--uxr-ua-platform-version / --uxr-ua-brand / --uxr-hw-concurrency / --uxr-device-memory
--uxr-webgl-vendor / --uxr-webgl-renderer / --uxr-webgl-fullparams
--uxr-canvas-seed / --uxr-audio-seed / --uxr-timezone / --uxr-languages
--uxr-screen-width / --uxr-screen-height / --uxr-webrtc-policy=disable_non_proxied_udp
| Env var | Purpose |
|---|---|
TILION_NO_DEFAULTS=1 |
Skip the default persona (bare launch) |
TILION_TZ / TILION_LANG |
Quick timezone / language override |
Note
Architecture in motion — persona transport v2. Today the persona is delivered through --uxr-* switches, world-readable via /proc/<pid>/cmdline (a process-level artifact, one persona per launch). Shipping next: an IPC-delivered, seed-driven persona graph feeding a process-global C++ MaskConfig singleton (the Camoufox model) — one binary, thousands of coherence-invariant fingerprints, per-BrowserContext identity isolation, and zero footprint on the command line.
Fortress exposes raw CDP on :9222, so it drops in under anything that speaks Playwright, Puppeteer, or CDP. Keep your framework, swap the browser.
| Framework | Connect via |
|---|---|
| browser-use (~70k stars) | cdp_url="http://localhost:9222" |
| Crawl4AI (~58k stars) | CDP endpoint |
| Stagehand (~21k stars) | connectOverCDP |
| LangChain Playwright toolkit | Playwright CDP |
| Playwright / Puppeteer (Python & JS) | connect_over_cdp / connect |
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp("http://localhost:9222") # Fortress under the hoodexport CHROMIUM_VERSION=$(cat CHROMIUM_VERSION)
build/build.sh # depot_tools, sync the tag, apply patches, gn gen, ninja
build/rebase-monthly.sh 152.0.XXXX.0 # bump + 3-way apply + rebuild + gauntlet-gateOutput: out/Fortress/chrome. The fork is 34 small single-surface patches (patches/), so most re-apply cleanly across upstream releases; the gauntlet then gates the release on any regression.
| Platform | Status |
|---|---|
| Linux x64 (native) · Windows x64 (native) · any OS via Docker | |
Code-signed installers · macOS .app · linux/arm64 |
in progress |
Fortress ships from four official channels — treat anything else as untrusted:
| Official source | |
|---|---|
| Source | github.com/tiliondev/fortress |
| Docker | tilion/fortress |
| Python | tilion-fortress |
| Node | tilion-fortress |
Verify a download — every release ships SHA256SUMS (the pip/npm SDKs run this for you on install):
BASE=https://github.com/tiliondev/fortress/releases/download/v151.0.7908.0
curl -LO $BASE/tilion-fortress-linux-x64.tar.gz
curl -Ls $BASE/SHA256SUMS | sha256sum -c --ignore-missing # -> OKVerify the Docker image by digest (not just the tag):
docker pull tilion/fortress:151.0.7908.0
docker inspect --format '{{index .RepoDigests 0}}' tilion/fortress:151.0.7908.0
# compare the printed sha256:... against the digest in the GitHub Release notesOr trust nothing and rebuild it. The whole fork is 34 readable patches in patches/; build/build.sh reproduces the binary from Chromium source, so you can diff what you built against what we ship.
Troubleshooting
Still blocked on Cloudflare, DataDome, or Kasada. Most of the time this is your IP, not your fingerprint — a datacenter range gets flagged before any page script runs. Route egress through residential or mobile proxies and retry; if it clears, the fingerprint was fine.
The fingerprint looks off on a Linux host. The default persona is Windows, but the TLS shape and some OS-facing signals follow the machine underneath. Match the persona to your egress OS, or set the relevant --uxr-* flags so the OS story agrees with where the traffic leaves from.
macOS pulls a Docker image. Native Linux + Windows binaries ship today; macOS still runs Fortress through the official Docker image (tilion/fortress). Install Docker Desktop, or run on Linux/Windows x64 for the native binary.
The persona shows up in /proc/<pid>/cmdline. The --uxr-* flags are readable by other processes on the host, one persona per launch. Until the runtime MaskConfig lands, keep one persona per process and avoid sharing the host with untrusted code.
A detector flags something the gauntlet passes. Detection moves. Confirm you're on the current Chromium rebase, then open an issue with the test page — that page becomes the next patch.
FAQ
Is this legal? Fortress is a browser engineering project for legitimate automation, testing, and scraping of publicly available data. Respect each site's ToS and the law in your jurisdiction.
Is it really free? Yes. BSD-3, fully open, and self-hostable. The patch series is published, so you can build the current engine from source yourself.
Why not just use puppeteer-stealth or undetected-chromedriver? They patch the JS/CDP layer after the page can inspect the browser, so they self-reveal via toString and realm re-acquisition. Fortress moves the spoof into C++, where the page finds native code. (See "Why an engine fork wins.")
How is this different from Camoufox? Same C++-interception idea. Camoufox forks Firefox (~3% of traffic, a standing anomaly) while Fortress forks Chromium and V8 (the majority engine), so a Chrome user-agent is coherent by construction.
Will it pass everything forever? No. Detection is an arms race, so we ship a dated, reproducible gauntlet and a monthly Chromium rebase, and you can always see exactly what passes today.
I'm still getting blocked. Roughly 90% of the time it's Layer 1 — your IP (datacenter range) — ahead of your fingerprint. Route egress through residential or mobile proxies and retry.
Roadmap
- Runtime JSON config into a C++
MaskConfig(one binary, many coherent fingerprints, nothing on the command line) - First-party MCP server plus Puppeteer / raw-CDP SDKs (drop-in for AI agents)
- Code-signed Windows
.exe+ macOS.app;linux/arm64Docker - Migrate
patches/to Brave-stylechromium_src/overrides - Published reCAPTCHA v3 / DataDome / Kasada benchmark rows (dated, reproducible)
Repo layout
patches/ 34 per-surface C++ patches (+ series), the source of truth for the fork
build/ args.gn, build.sh, apply-patches.sh, rebase-monthly.sh, windows/, macos/
packaging/ tilion launcher, fonts.conf, Dockerfile, .deb + bundle builders
fonts/ 33 metric-compatible Windows-named fonts (incl. color emoji)
sdk/ python + node (tilion-fortress) prebuilt-binary SDKs
tools/ gauntlet.py, the CreepJS / Sannysoft / BrowserScan CI gate
docs/ GAUNTLET_RESULTS, BUILD_NATIVE, BENCHMARK
Found a detection vector or a leak we missed? Open an issue with a reproducible test page. A page that reliably flags Fortress is the most valuable thing you can send — it becomes the next patch. Two house rules: every capability claim ships with a command that reproduces it, and every limit is written down. The word "undetectable" stays out of the repo.
BSD-3-Clause for the Fortress patches and tooling (matching Chromium). Chromium and the bundled fonts retain their own licenses — see LICENSE and NOTICE. The patch series is published, so you can audit and rebuild the engine yourself.
Important
v2 — MaskConfig runtime personas (the big one). An IPC-delivered, seed-driven persona graph feeding a process-global C++ MaskConfig singleton — thousands of coherence-invariant fingerprints from a single binary, per-BrowserContext identity isolation, and zero command-line footprint. One browser, an entire population of coherent identities.
| Native | Linux + Windows .exe ship now; next: code-signed installers, macOS .app, and linux/arm64 Docker. |
| Agent-native | First-party MCP server + raw-CDP / Puppeteer SDKs — drop-in for autonomous browser agents. |
chromium_src |
Brave-style in-tree overrides for near-zero-conflict monthly rebases. |
| Benchmarks | Dated, reproducible reCAPTCHA v3 / DataDome / Kasada result rows. |
Detection never stops moving, and neither does Fortress — a monthly Chromium rebase, a fresh gauntlet, and the next patch every time a detector finds a tell.
Star the repo to back the work · Watch → Releases to catch the v2 MaskConfig drop the day it lands.
Stealth you can read, rebuild, and run yourself.





