Skip to content

Fingerprint flags

Identity is controlled with Chromium command-line switches. The SDK sets them from named options; with the open build you can also pass them as args to your own launcher.

Most switches are in both builds. Some are licensed-build only (Free with GitHub or Pro) — the switch list below marks those with the engine revision that added them. Without a licence key the SDK runs the open build, where those options have no effect.

The seed model

Everything keys off --fingerprint=<seed>. The seed (an integer or any string) deterministically derives the persona — and perturbs the two rendered surfaces listed under what is farbled below:

Since the licensed Chromium 156 build (r51) that derivation runs in the Clearcote persona service: the launcher asks it at every start with the licence's run token and hands the signed persona to the browser, which refuses to start without one. Same seed, same identity as on r50. Offline licences stay on r50 until an offline persona pack exists.

  • Same seed ⇒ same identity on every launch on the same machine and browser version — a returning visitor. Everything a page can read comes back identical: the GPU, screen, hardware, fonts and voices, and the canvas, WebGL and audio results. The one exception is the connection speed estimate (the round-trip time and bandwidth in navigator.connection), which follows your real network, as it does in Chrome.
  • New seed ⇒ a fresh, plausible identity.
  • The canvas and WebGL noise is derived per site (inspired by Brave's farbling), so those hashes differ across domains. The persona itself — GPU, screen, hardware, fonts — is the same everywhere, as one real machine's would be.

The same seed on different machines

The graphics card a persona claims comes from the maker of the card the machine really renders with (NVIDIA, Intel or AMD), so the pixels and graphics limits a page measures belong to the kind of card the page is told about. On a machine with no graphics card the persona claims integrated graphics: from 154 r37 Intel's or AMD's, on r36 Intel's only. So a seed reports a different graphics card on machines whose cards come from different makers — by design.

On the licensed build from 154 r36 the machine's graphics maker can change the rest of the hardware too. A dedicated graphics card does not come with an entry-level computer, so on a machine with an NVIDIA card, a seed that would claim entry-level hardware (4 to 6 processor threads, 4 to 8 GB of memory, a small screen) claims mid-range or better instead, and never a touchscreen. About half of all seeds therefore report different processor threads and memory on an NVIDIA machine than on an Intel, AMD or graphics-less one, and some a different screen size. On machines with the same graphics maker, a seed's identity is the same.

From 154 r37 the range of cards a maker's machines draw from is taken from real browser traffic: each model is as likely as it is in the wild, so a fleet of personas looks like a fleet of real machines. Most seeds therefore claim a different card on r37 than on r36; the rest of the hardware derives as on r36.

Since 2026-10-26 (r52, and r51 launches too, because the persona service derives the identity) the default is persona schema 7: the graphics card sets the kind of machine — laptop or desktop, processor generation, threads, memory and screen, including the display scale laptops really run at. A seed draws a different machine than it did before that date.

To keep one identity for a seed across machines:

  • Run it on machines with the same graphics maker — for example one server type in production.
  • Pin the graphics maker by passing --fingerprint-gpu-host-vendor=nvidia (or intel, amd) in your launcher's args. The persona then derives its identity as it would on a machine with that maker's card, wherever it runs. Pin the maker of the machines the persona mostly runs on: on a machine whose card is from a different maker, the card it claims no longer matches the one that renders.
  • Keep identities created on r45 to r51 (before 2026-10-26) with persona_schema=5 / personaSchema: 5 (--fingerprint-schema=5), and those from 154 r37 to 155 r44 with persona_schema=4 / personaSchema: 4. Both schemas are frozen: a seed derives the same identity on them for good.
  • Keep identities created on 154 r36 with persona_schema=3 / personaSchema: 3 (--fingerprint-schema=3). Every seed then derives exactly what it did on r36, field for field, the graphics card included.
  • Keep identities created before r36 with persona_schema=2 / personaSchema: 2 (--fingerprint-schema=2). Every seed then derives exactly what it did on 153 r29 to 154 r35, field for field: the graphics card still follows the machine's maker, the rest of the hardware does not.

Without a seed there is no per-persona identity: every launch on the same kind of machine looks alike. Give each identity its own seed.

Prefer a real machine's identity over the synthetic seed-derived one? Capture a donor Chrome with the collector and import it via --fingerprint-profile=<gzip+base64 JSON> (or the SDK's fingerprint_profile / fingerprintProfile option, which does the gzip+base64 for you) instead of a seed. Fields present in the profile override what the browser would otherwise report; absent fields fall back to the browser's defaults. Do not pass a --fingerprint seed alongside a profile: the seed adds canvas noise, which strict scoring reads as tampering next to a real machine's values, and the profile already supplies the identity (see Profiles and persistence). Both builds read imported profiles; on the licensed build they apply in full from 151 r19. See the Playwright guide for how to import a captured profile from the SDK.

What is farbled, and what deliberately isn't

Perturbing a surface buys unlinkability and costs coherence, and that trade is not worth making everywhere. Clearcote farbles the two surfaces where the noise sits closest to ordinary hardware variation, and leaves the rest unperturbed — because on those, a perturbed value is a louder signal than a shared one. Unperturbed is not the same as untouched, though: a few values are still substituted wholesale from the persona, which the table and the note under it spell out. Some checkers can still flag the noise itself; when that matters, fingerprintNoise: false turns it off and keeps the identity (see How detection works).

SurfacePer-seed noiseWhy
Canvas 2D — toDataURL, getImageData and the other ways a page can export a canvas (on the licensed build every export path agrees from 152 r21)YesReadback already varies between real machines by GPU, driver and rasterisation path, so a small perturbation sits inside the natural spread.
WebGL — readPixels, toDataURLYesSame reasoning. On the licensed build from 154 r34, disableGpuFingerprint changes only what the GPU reports: readPixels and a WebGL canvas's exports keep the seed's noise and agree with each other. On the open build and older licensed builds it also stops the readPixels noise while the exports stay noised, so pair it with fingerprintNoise: false there for every read of the canvas to agree. fingerprintNoise: false turns WebGL noise off everywhere.
Audio — AudioContext sample valuesNo — by designWeb Audio output is deterministic for a given pipeline. Perturbing it means either an off-spec sample rate (44099.99 rather than 44100) or samples that no stock renderer produces — both are values a real browser cannot emit, which is a stronger tell than two profiles sharing an audio hash. The reported scalars are a separate matter and do follow the persona — see below.
Client rects & text metricsNo — by designRects are an output of layout: they follow the fonts, the device pixel ratio, the zoom level and the locale, and change only when those do. An imported profile with another pixel ratio, or another language (which can change the default fonts), lays the page out differently; different seeds on one machine lay it out with the same fonts at the same pixel ratio, so they report the same rects. Detectors catch rect noise without any stored value, by testing against answers known in advance. CreepJS requires a 100×100 box rotated 45° to hash to 9d9215cc at pixel ratio 1, right - left to equal width exactly, two identical elements to measure the same and an empty element to measure zero. Genuine Chrome reports a rect's x, y, width and height as exact single-precision (float32) numbers, which noise added in JavaScript rarely keeps. Noise added after layout fails these checks however small it is. Brave, Firefox's and Tor Browser's fingerprinting protection and Camoufox leave rects alone too; Camoufox removed its glyph-width noise in September 2026 because no real machine produces those widths.
WebGPU — adapter and limitsNoFollows the persona's GPU rather than the seed, so it keeps agreeing with what WebGL reports. Two GPU APIs on one page naming different vendors is readable in a single call.

The practical consequence for multi-account work: canvas and WebGL separate your identities; rendered audio and client rects do not. Those are properties of the machine underneath — the same host renders the same audio pipeline and lays text out with the same fonts at the same pixel ratio whichever seed is loaded, exactly as it would for two different people on identical hardware. If a target correlates on them specifically, the answer is separate machines, not a different switch.

One exception worth knowing: the three AudioContext scalars do follow the persona. sampleRate, baseLatency and outputLatency are served from the persona whenever one is active — a --fingerprint seed or an imported profile — so they vary between identities instead of exposing the host's audio backend. With neither set, all three pass through from the real machine. A rate the page requests explicitly is still honoured, and outputLatency still goes through Chrome's own permission-dependent quantiser. From 153 r26, baseLatency also tracks the render size the page requests, and a context that has not started rendering reports the same latency a regular Chrome does.

Coherent secondary surfaces

Beyond the noised signals, the engine keeps the persona's secondary surfaces agreeing with a real Chrome on the persona's platform. The SDK defaults the platform to your host OS; the Windows-specific values below apply to a Windows persona.

  • WebGL limits (WebGL1 + WebGL2) follow the persona's GPU wherever this machine can back them. A limit is never reported higher than the real driver enforces, so on a software renderer some values are lower than the named GPU's.
  • UNMASKED_RENDERER / UNMASKED_VENDOR are session-constant — one GPU on every site, tracking the persona, instead of a per-origin tell.
  • navigator.getBattery() reports a desktop on mains power (charging, level 1.0, no discharge), and navigator.connection a residential profile (effectiveType 4g, rounded rtt/downlink, saveData off).
  • navigator.keyboard.getLayoutMap() returns a US-QWERTY map, and on a Windows persona AudioContext reports the sample rate and latency of Windows' audio stack.
  • Light or dark — from 154 r32 the persona prefers one colour scheme (about one in three dark; --fingerprint-color-scheme=light|dark chooses), and prefers-color-scheme, matchMedia, the Sec-CH-Prefers-Color-Scheme header and CSS system colours all agree. The SDK (0.41.0+) leaves Playwright's colour-scheme emulation off on such a build so nothing contradicts it.
  • Fonts — on a Windows persona, text has a Windows machine's line heights and common font widths, and fonts a page asks for by name behave as they do on Windows (154 r32). A genuine font installed on the machine is used as itself; on Linux, list yours with fontDirs.
  • window.getScreenDetails() reports a single coherent monitor; @media (pointer: fine) / (hover: hover) match a desktop with a mouse.
  • URLs — under a Windows persona, new URL("C:/").protocol returns file:, and from 153 r26 every way a page can build a URL gives the same answer. navigator.share / canShare are exposed to match the Windows UA.
  • WebGPU — navigator.gpu adapter info + limits/features track the same GPU as WebGL (on the licensed build, narrowed to what the host can deliver).
  • Locale — Accept-Language, navigator.language, the full navigator.languages list and Intl (main thread + workers) all come from one list (acceptLanguage, or geoip). The SDK sets --accept-lang and --lang for you; with raw switches, pass both.
  • Speech — speechSynthesis serves the persona's voice set, including the Google network voices a Chrome of that version lists (153 r27). fingerprintVoices: false keeps the machine's own voices instead (152 r22).
  • Hardware — the processor-speed reading (navigator.cpuPerformance) follows the persona rather than the real processor (152 r20), and the script memory ceiling (performance.memory.jsHeapSizeLimit) follows the memory the persona claims (153 r26).
  • Codecs & devices — MediaCapabilities.decodingInfo() reports a persona codec matrix, and enumerateDevices() a persona media set (seed-stable ids, empty labels pre-permission).
  • UA-CH high-entropy — bitness=64 / wow64=false / model; on a Linux persona platformVersion is empty, as genuine Chrome on Linux reports (153 r26). navigator.storage.estimate() reports a realistic on-disk quota when you set storageQuota.

Experimental: real-GPU canvas bridge

Optionally forward canvas / WebGL ops to a remote real-GPU host so readbacks (getImageData / toDataURL / readPixels / measureText) are coherent with the GPU you present — even on hardware that can't render it locally. Because it forwards the operations (not pre-recorded images) it handles most canvases, not just known probes. Enable with --canvas-bridge-url=ws://host:port (plus --no-sandbox); unset means fully local, exactly as before. The render server drives a browser on the real-GPU host (a regular Chrome over CDP, or a headless Clearcote). See the canvas-bridge guide for the full setup.

Native metadata overrides & light_stealth

Alongside the seed-derived persona, a set of native single-value overrides let you spoof individual navigator/screen values directly — hardwareConcurrency, deviceMemory, colorDepth, devicePixelRatio, maxTouchPoints, and (opt-in) the screen/avail* dimensions. Each is read straight by its getter with the precedence flag > --fingerprint persona > real host, so a hardcoded value wins over any seed and the overrides work with or without a --fingerprint seed — they never engage the persona machinery.

The SDK bundles a light subset behind one flag, light_stealth: it applies one of a small set of coherent metadata bundles (hardwareConcurrency, deviceMemory, colorDepth, devicePixelRatio, maxTouchPoints) via those native switches only. The seed only picks which bundle and is not passed to the engine, so there is no canvas or WebGL noise and every light_stealth session on one machine shares the host's canvas hash — use a per-account fingerprint seed without lightStealth when those need to differ. The TLS ClientHello and the real browser version are unchanged. screen dimensions are deliberately not spoofed by default (opt-in via screenWidth etc.), because a faked screen that can't be reconciled with the real render surface is easy to spot; on an unscaled display, check that the chosen devicePixelRatio suits it, or set it yourself. An explicit option always wins over the preset.

javascript
import { launch } from "clearcote";
// one coherent metadata bundle; "my-seed" only picks which one — no seed reaches the engine, so no canvas noise:
const browser = await launch({ lightStealth: true, fingerprint: "my-seed" });

// or set individual values by hand (no --fingerprint needed):
const b2 = await launch({
  hardwareConcurrency: 8,
  deviceMemory: 8,
  devicePixelRatio: 1.25,
  maxTouchPoints: 0,
});

All switches

Filter by name or effect, or narrow to one subsystem. Click a switch to read what it does and see its shortest valid form.

Identity & persona

Master seed (int or string). Derives the whole persona (GPU, screen, hardware, fonts) and drives the per-site canvas and WebGL noise. Language and timezone are not seed-derived: they come from --accept-lang / --timezone (SDK: acceptLanguage, timezone or geoip). Rendered audio and client rects are not perturbed; the AudioContext sampleRate/baseLatency/outputLatency scalars are served from the persona when one is active — see the fingerprint docs. Same seed ⇒ same identity on the same machine and browser version; on machines with a different graphics maker the identity differs (see the fingerprint docs and --fingerprint-gpu-host-vendor).

Hardware & screen
GPU & WebGPU
Network & locale
Noise
Canvas bridge
Media & compatibility
Other

Examples

These are raw command-line flag sets. For complete Python and Node workflows, see Examples.

A full, coherent Windows identity:

bash
--fingerprint=acme-tenant-7 \
--fingerprint-platform=windows \
--fingerprint-brand=Chrome \
--accept-lang=en-US,en \
--lang=en-US \
--timezone=America/New_York \
--fingerprint-hardware-concurrency=8

A Windows identity with custom GPU strings, in the form ANGLE on Windows reports them:

bash
--fingerprint=42 \
--fingerprint-platform=windows \
--fingerprint-gpu-vendor="Google Inc. (NVIDIA)" \
--fingerprint-gpu-renderer="ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x00002504) Direct3D11 vs_5_0 ps_5_0, D3D11)"

The strings change what is reported, not what draws: pixels still come from this machine's GPU. Name the GPU you actually render on, or use the canvas bridge. The macos platform on the open build changes the identity strings only (UA, client hints, navigator.platform); on the licensed build since 155 r47 it describes a real Mac, with the chip, threads, memory, screen, graphics renderer, macOS version and voices that go together. And android is a best-effort mobile persona.

Pin a location together with its timezone so geolocation and clock agree:

bash
--fingerprint=nyc-1 \
--timezone=America/New_York \
--fingerprint-location=40.7128,-74.0060

Import a real machine's values

Instead of a synthetic seed, you can report the exact values a real Chrome reported — GPU strings + getParameter table, screen, fonts, voices, audio. The values are substituted; the rendering is still done by your own machine, so a profile does not give two accounts different canvases — a per-account --fingerprint seed does. Use one or the other, not both: a profile for one real machine's values, a seed for per-account render separation. Grab a profile from the curated clearcote-profiles library (thousands of real-machine profiles, GPU-vendor tagged), or capture your own with the collector, then load it and prove it loaded:

javascript
import { launch } from "clearcote";
// path to a captured .json profile, a profile object, or a JSON string
const browser = await launch({ fingerprintProfile: "./real-machine.json" });
// fields present in the profile override what the browser would report;
// absent fields keep the browser's defaults. Pass no fingerprint seed alongside a profile.

Capture flow: open the collector page in a real Chrome you want to clone, export the JSON, then pass it via fingerprintProfile (SDK) / --fingerprint-profile (engine). Verify with tools/fingerprint-collect/verify_profile.py.

What each patch does

The open build's switches each map to a readable engine patch: the diffs live in patches/, with a one-line summary of each in patches/README.md. Switches marked “licensed build” come from the licensed build's own patch set, which is not public.

Coherence matters more than any single value: keep platform, timezone, locale and GPU plausible together. See Architecture for how the engine keeps them consistent, and How detection works for why this beats JavaScript spoofing.
The diff

38,504 lines between stock Chromium and Clearcote Pro.

That is how far the Pro build is from the Chromium Google ships. Every value a site can read is set where Chromium computes it, so the page, its workers and its iframes all describe the same device.

  • Canvas
  • WebGL
  • WebGPU
  • Audio
  • Fonts
  • Screen and window
  • WebRTC
  • TLS
  • Client hints
  • Input timing
  • Voices and codecs
Read the open build's patches on GitHub

Pro build, counted from its r54 patch stack (Chromium 156.0.8078.47). The free open build is a smaller, public patch set.

Chromium 156.0.8078.47 → Clearcote Pro r54

344 files changed, 35531 insertions(+), 2973 deletions(-)

234 patches

Files changed, by area

  • Rendering engine (Blink)118
  • Browser (chrome/)52
  • Content layer42
  • Components38
  • Network stack22
  • UI and windows14
  • TLS (BoringSSL)11
  • V8 and URL17
  • Everything else30