← All posts
ArchitectureJul 15, 2026 · 7 min read

Why MaskOle masks in the C++ engine, not JavaScript

Most antidetect browsers patch JavaScript APIs after the page loads — a seam any detector can find. Here is why we forked Chromium and moved masking into the engine itself.

Every antidetect browser makes the same promise — a different, believable fingerprint for every profile. The interesting question is where that fingerprint gets manufactured. There are three places you can do it, and they are an order of magnitude apart in how hard they are to catch.

Three tiers of masking

You can intervene at one of three layers, from cheapest and most detectable to hardest and most robust:

MaskOle is Tier C. Every masked value — canvas, WebGL, audio, fonts, timezone, navigator — is produced inside Chromium's own C++, not sprinkled on top in JavaScript.

What a JavaScript patch leaves behind

When you overwrite a browser API from JavaScript, the overwrite is observable from the same JavaScript environment a detection script runs in. The classic tells:

None of these are exotic. Open-source fingerprinting suites like CreepJScheck for exactly this kind of tampering. A patch that lives in the page can always, in principle, be caught from the page.

Masking below JavaScript

MaskOle answers the fingerprint call before any page script runs. When an anti-bot script asks for a canvas hash or a WebGL renderer string, the request is served by the engine — in C++ — and returns a single, coherent, real-device value. There is no wrapper on the prototype, no toString mismatch, no trap to detect. The property order stays native because nothing in the JavaScript environment was touched.

Concretely, every dimension follows the same two-step pattern inside the fork:

  1. A one-time framework patch registers a family of switches and parses a per-profile fingerprint config (--fp-config=<path.json>) into a globally readable object at process start.
  2. Each fingerprint surface — canvas readback, WebGL getParameter, the audio render path, font enumeration, the timezone controller — reads that config and returns the masked value (or applies deterministic noise to the real one).
A profile is just a JSON file describing a whole device. One process, one identity. Profiles never share state, and the mask is chosen before the first byte of page JavaScript executes.

Two rules the engine enforces

Determinism

The same config, launched a hundred times across machines, must produce a byte-identical fingerprint. All noise is a pure function of a seed, never true randomness. If a profile's fingerprint drifted between launches, that drift would itself be the anomaly. (The next post is entirely about why this rule matters more than it looks.)

Coherence over coverage

We would rather mask one dimension fewer than let two dimensions contradict each other. A user agent that says Windows while navigator.platform says Mac, or a WebGL renderer that reports an Apple GPU on a "Windows" profile, is what detectors actually hunt for. UA, UA-CH, platform, WebGL vendor, timezone and language all come from one persona.

The part that doesn't change: your tooling

Going Tier C doesn't cost you your automation stack. The kernel still exposes a standard CDP endpoint, so Playwright, Puppeteer and Selenium connect over connectOverCDP with zero new concepts. And when no config is passed, the build behaves exactly like upstream Chromium — which makes it trivial to prove a given signal came from our patch and not from Chromium itself.

Engine-level masking is more expensive to build and carries a real maintenance tax. We think that is exactly why it's worth doing: the ceiling on how undetectable you can be is set by the layer you mask at, and JavaScript is the wrong layer.

Try the mask yourself.
Free forever plan. Ten identities. No card.
Download MaskOle Free