# Fortress: the spoof is in the C++, not in the page

> Fortress is a Chromium build that corrects browser fingerprints inside the engine rather than on top of it, so existing Playwright or Puppeteer code connects over CDP and nothing else changes. The v3 release is really a fleet engine: the persona arrives over IPC, a restored snapshot re-keys canvas, audio, GPU and TLS in place, and the project's own measurement puts it ahead of the next best stack on protected pages.

**tiliondev/fortress** — Stealth Chromium engine that stops scrapers and browser agents from getting blocked, with one line of code change.

- Repository: https://github.com/tiliondev/fortress
- Website: https://tilion.dev/
- Stars: 720 · Forks: 47
- Language: Python
- License: NOASSERTION
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/tiliondev-fortress

## Every spoofed getter is a C++ getter, and that is the whole thesis

The design decision that separates this from a script that overrides browser properties is stated in one line: every spoofed getter is a C++ getter, so toString on it returns native code, and the result holds across the main frame, iframes and web workers alike. Realm invariance is the hard part of that claim. A page can ask a function for its own source text from inside a frame, from a worker, and from the top document, and an override implemented in one realm fails the comparison in the others. The project counts 81 single-surface C++ patches as the cost of getting there. The same logic drives the graphics side. WebGL and WebGPU are made to agree with the persona's GPU at the parameter level, which means limits, precision and extension lists, plus the adapter identity that navigator.gpu reports, rather than only the renderer string a detector prints once. The rendering backend is ANGLE with D3D11 behind it, so the values come from a real path instead of a lookup table. Keeping the engine honest is the other half: Blink, V8 and BoringSSL are patched in tree, which is what lets the engine build, the user-agent string and the JA3 and JA4 TLS shape stay in agreement instead of drifting apart the moment one of them is spoofed alone.

## The persona arrives over IPC because the command line is readable

Older stealth approaches pass the profile as command-line switches, which means the switches exist somewhere a cooperating or curious party can find: process listings, browser diagnostics, crash reports. Fortress v3 delivers the persona to the renderer over IPC into a process-global config instead, and describes the result as zero command-line footprint with per-context identity isolation. The isolation half is the part worth pausing on. Multiple sessions or accounts inside one browser must never collapse into a single linkable identity, and that guarantee is enforced in one place: the process-global config plus the per-context layer above it. Get that layer wrong and every account in the browser shares a fingerprint, which is worse than no spoofing at all because it looks deliberate. The design keeps explicit overrides in the form of uxr-prefixed switches, which is a sensible escape hatch for debugging and a real hazard in production, since an override passed on the command line reintroduces exactly the footprint the IPC path removes. The practical advice is to treat those switches as a development tool, check your launch configuration for them before pointing the browser at anything that matters, and remember that the fleet numbers in the next section depend on this layer holding.

## Sixty-four clones come up forty-eight distinct, and the gate is five hundred

The fleet claim is specific enough to be checked. On snapshot resume, the whole persona re-keys in place: the random source, canvas and audio, GPU and screen, user agent, network and TLS state, with no relaunch of the browser. The project reports sixty-four clones coming up forty-eight out of forty-eight distinct, and says the mechanism is stress-gated to five hundred clones. Read that carefully, because the denominator is the interesting part. Forty-eight out of forty-eight describes the sampled attributes in one fleet of sixty-four, not uniqueness across every observable a detector can read, and a fleet of sixty-four is not a fleet of five hundred even though the latter is the stated stress target. What makes the design credible is not the ratio but the acknowledgement step: each surface confirms that it re-keyed, in a closed loop, so a surface that silently did nothing is a detectable failure rather than a slow drift you find out about from a block page. Combined with the per-context isolation in the persona delivery, that gives you a story about how a mistake would show up. It does not give you a story about what happens at four hundred concurrent sessions, which is the number that would matter to a commercial scraping operation.

## A zero seed disables the noise instead of falling back to a constant

Canvas noise is where anti-detect projects leak, and Fortress treats that as an engineering problem with three specific answers. The noise is idempotent and byte-exact across the three ways a page can read a canvas, getImageData, toDataURL and toBlob, including the offscreen variant, so asking twice cannot produce two different pictures. It is edge-gated to anti-aliased pixels only, which stops the noise from touching flat regions where it would be visible as a texture rather than as grain. And the seeds are 64-bit and domain-separated, so two surfaces cannot collide by accident. The most interesting decision is the failure mode. The random source is fail-closed: a zero or absent seed disables the noise entirely rather than falling back to a fixed value. The reasoning is legible. A shared fallback constant is a fingerprint, because every installation that hits the same fallback produces the same image, and a blank canvas is at least an obvious failure you can notice. WebGL and WebGL2 readback, including the pixel buffer object path that behaves differently from the ordinary one, routes through the same gate. Audio takes the opposite approach and is value-keyed: identical inputs give identical outputs, fresh for each persona but internally stable, which is the property a page needs when it compares two reads of the same buffer.

## A hundred and fifty-four bundled font families keep host fonts out of the answer

Font enumeration is a machine identifier that has nothing to do with the browser engine, which is why the repository carries a fonts directory and the bundle step takes a fonts argument. The project bundles 154 distinct font families with metric clones and per-persona metrics, and gates enumeration to the persona so the fonts installed on the host are never visible to the page. Without that gate, a page can enumerate a font list no real Windows or macOS machine would have, and the spoofed platform stops mattering. The same reasoning runs through the rest of the coherence list, which the project describes as about thirty specific tells closed, each with a coherence rule in place of a spoof. A media query matches the screen and the pixel ratio it claims, colour gamut and dynamic range agree with the display, the heap size limit matches the reported device memory, the colour scheme preference varies per persona with about a third dark, system fonts follow the claimed operating system, the device-memory client hint is set on the navigation request path where a detector actually looks for it, WebAuthn reports per-persona support for user verification, macOS gets zero-width overlay scrollbars, WebRTC fails closed so a bare launch leaks no real address, and pointer, hover and maximum touch points are pinned. Even the laptop is a story: Mac personas claim an architecture, core and memory combinations, and a battery that is not permanently full.

## Continuous integration gates the patch series, not the gauntlet

The developer task list is short enough to read as a statement of what the maintainers actually check. ```
lint: ## Run the patch-set integrity linter
	$(PYTHON) tools/check_patches.py

test: ## Run the Python SDK unit tests
	$(PYTHON) -m pytest sdk/python/tests -q

check: lint test ## Lint + test (what CI gates on)

gauntlet: ## Run the live detection gauntlet against a bundle (BUNDLE=/path/to/tilion-fortress)
	$(PYTHON) tools/gauntlet.py --bundle $(BUNDLE)
``` The comment on the check target says it is what continuous integration gates on, which means two things pass on every change: a patch-set integrity linter and the Python SDK unit tests. Neither of those launches a browser. The detection gauntlet is a separate target that needs a built bundle passed in, and it is the thing that reproduces the live capture the project shows, clearing a Cloudflare challenge, reading a bot-detection page all green, and reporting a normal result on a fingerprint scanner. So the numbers in the release announcement come from a run you have to perform yourself against a bundle you have to assemble, and they are not re-verified on each commit. Two details in the task list are worth copying regardless of what you think of the project. The apply and bundle targets refuse to run without their arguments and exit with a usage message, which is the right behaviour for a step that patches a Chromium checkout, since a half-applied patch series is expensive to unwind. And nothing in the list compiles Chromium, which the file says outright and points at a separate document for.

## Two install routes, three native platforms, one built from source

The client-side promise is one line of change, and it holds because the browser speaks raw CDP on port 9222 in the style node drivers use, with no runtime-enable leak on the way. Enabling that domain is itself a signal, so leaving it off is part of the design rather than an optimisation. On the binary side there are two routes and they are not equivalent. ```bash
pip install -U tilion-fortress       # or:  docker run --rm -p 9222:9222 tilion/fortress:latest
``` Windows on x64 ships as a native archive with a launcher script, Linux tarballs for arm64, x86 and armhf are described as rolling out, and the macOS application is built from source. So on a Mac the one-line promise still holds, but you are compiling the thing rather than downloading it, and the platform with the most scrutiny from fingerprint detectors is the one with the least packaged distribution. The container route is the only one that gives every platform the same artifact, and it costs you the local networking shape: the port mapping is the whole integration surface. The release cadence is monthly rebases onto upstream Chromium, and the v3 cycle moved from 151 and 152 to 153 with 87 commits on the patch series, which is the real ongoing cost of this approach and the thing to watch when you decide whether to depend on it.

## Widevine and ten thousand recorded paths close the last two obvious gaps

Two of the more convincing items in the v3 list are unglamorous. The first is DRM. A real Widevine content decryption module is bundled and enabled, so a page calling requestMediaKeySystemAccess for the standard Widevine key system gets the same answer it would from a genuine Chrome install, and the project notes that this closes a gap its own roadmap had flagged. Absence of a CDM is one of the oldest cheap signals there is, and it cannot be patched from the page side. The second is cursor movement. Rather than interpolating between coordinates, the engine ships a trajectory engine backed by a bank of around ten thousand recorded human paths, and it reports that this produces a motion signal about sixteen times more human than driving the mouse directly, with its own humanness metric improving from roughly seventy to roughly four point three. Those are the project's numbers on its own metric, not an independent measurement, and the mechanism is still the right one: replaying a distribution of real velocity and micro-jitter curves beats a synthetic ease-in-out because detectors have learned what synthetic easing looks like. Speed-proportional batching of coalesced pointer events and per-persona audio timing finish the behavioural picture.

## Conclusion

Fortress is the right tool if you already run automation at a scale where fingerprint coherence is the bottleneck, because the corrections live in the engine, the persona is delivered where a page cannot read it, and the release process is gated on a live gauntlet rather than on a claim. It is the wrong tool for a one-off script, for a site where you have permission to test and a plain browser works, or for anyone who cannot own the decision to defeat a site's bot defences, which stays with the operator whatever the fingerprint says. Before you rely on it, read the 86.4% figure as what it is: one run of 92 pages, produced by the project that also wrote the browser, with Camoufox as the only named comparison. Run the gauntlet against your own targets, keep an eye on the monthly upstream rebase as the real maintenance cost, and remember that the Linux tarballs are still rolling out while the macOS build is a source build.

## FAQ

### What is Fortress and how do I use it with Playwright or Puppeteer?

It is a Chromium build whose fingerprint corrections live in the engine's C++, and it exposes raw CDP on port 9222. You point your existing Playwright or Puppeteer client at that port and change nothing else in your code.

### How do I install Fortress?

Either install the Python package with pip install -U tilion-fortress, or run the published container with docker run --rm -p 9222:9222 tilion/fortress:latest. Windows x64 ships a native archive, Linux tarballs for several architectures are rolling out, and the macOS application builds from source.

### What results does Fortress report against bot detection?

Headless, on datacenter addresses and with no proxies, it served 86.4% of 92 protected pages, against 74.5% for the next best stack it names, Camoufox. It also reports zero percent detection on the headless and stealth checks of one fingerprint scanner, with the other scanners and a live challenge cleared.

### How does Fortress keep many browser sessions from looking like one user?

The persona is delivered to the renderer over IPC with per-context isolation, so sessions and accounts inside one browser do not collapse into one linkable identity. When a snapshot is restored the whole persona re-keys in place without a relaunch, and a fleet of 64 clones is reported as 48 out of 48 distinct, stress-gated to 500 clones.

### What is the gauntlet in the Fortress repository?

It is the live detection gauntlet, a developer target that runs against an assembled bundle and reproduces the project's fingerprint captures. It is separate from the check target that continuous integration runs, which covers only a patch-set integrity linter and the Python SDK unit tests.

### Does Fortress need a proxy or a residential IP to work?

The reported figures come from headless runs on datacenter addresses with no proxies at all, which is the configuration the project uses for its measurement. Correcting the browser fingerprint is what the engine does, so network reputation is a separate variable the project does not claim to address.

## Sources

- [Issues](https://github.com/tiliondev/fortress/issues)
- [Project website](https://tilion.dev/)
- [README](https://github.com/tiliondev/fortress/blob/main/README.md)
- [Releases](https://github.com/tiliondev/fortress/releases)
- [tiliondev/fortress on GitHub](https://github.com/tiliondev/fortress)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tiliondev-fortress
