Fingerprint flags
Identity is controlled with Chromium command-line switches. Pass them as args to your launcher.
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:
- Same seed ⇒ same identity across launches — a returning visitor.
- New seed ⇒ a fresh, plausible identity.
- Signals are derived per site (inspired by Brave's farbling), so the same seed is decorrelated across different domains rather than globally linkable.
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). Fields present in the profile override the seed-derived persona; absent fields fall back to the --fingerprint seed, so partial profiles stay coherent. 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 is indistinguishable from 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.
| Surface | Per-seed noise | Why |
|---|---|---|
Canvas 2D — toDataURL, getImageData | Yes | Readback already varies between real machines by GPU, driver and rasterisation path, so a small perturbation sits inside the natural spread. |
WebGL — readPixels, toDataURL | Yes | Same reasoning. Skipped when disableGpuFingerprint is on: that mode exists to make the reported GPU match the one actually drawing, and noising pixels that are otherwise honest re-introduces the contradiction it removes. |
Audio — AudioContext sample values | No — by design | Web 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 metrics | No — by design | Chrome quantises layout onto a native 1/512-px grid, so every honest rect is an exact multiple of 0.001953125. A scale factor applied afterwards pushes every rect off that grid, and off-grid geometry is recoverable from a single measurement no matter how small the factor. |
| WebGPU — adapter and limits | No | Follows 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 the same layout grid whichever profile 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.
Coherent secondary surfaces
Beyond the noised signals, the engine keeps the persona's secondary surfaces agreeing with a real Chrome-on-Windows desktop, so nothing contradicts the seed under deeper inspection:
- WebGL limits (WebGL1 + WebGL2) report the canonical ANGLE/D3D11 caps a real Windows GPU returns, not the host's software-renderer values.
- 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 coherent desktop (charging, level 1.0, no discharge), and navigator.connection a residential profile (effectiveType 4g, rounded rtt/downlink, saveData off).
- navigator.keyboard.getLayoutMap() returns a clean US-QWERTY map, and AudioContext sampleRate / baseLatency / outputLatency match Windows WASAPI.
- window.getScreenDetails() reports a single coherent monitor;
@media (pointer: fine)/(hover: hover)match a desktop with a mouse. new URL("C:/").protocolreturnsfile:(Windows-coherent), and navigator.share / canShare are exposed to match the Windows UA.- WebGPU —
navigator.gpuadapter info + limits/features track the same GPU as WebGL (no host discrete-GPU leak under a seed-only persona). - Locale & speech —
Intl(main thread + workers) resolves to the same language asnavigator.language(noen-GBleak), andspeechSynthesisserves the persona's voice set. - Codecs & devices —
MediaCapabilities.decodingInfo()reports a persona codec matrix, andenumerateDevices()a persona media set (seed-stable ids, empty labels pre-permission). - UA-CH high-entropy —
bitness=64 /wow64=false /model, plusnavigator.storage.estimate()reporting a realistic on-disk quota.
Current pre-release caveat: Accept-Language, navigator.language, and the Intl locale are pinned coherently, but the full navigator.languages array is not fully implemented yet; only the primary language tag is exposed there today.
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 arbitrary canvases. Enable with --canvas-bridge-url=ws://host:port (plus --no-sandbox); unset means fully local, exactly as before. The render server is a headless Clearcote on the real-GPU host. 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 the safe subset behind one flag, light_stealth: it applies a coherent, seed-derived set of just the metadata axes that survive the strictest bot-detection checks (hardwareConcurrency, deviceMemory, colorDepth, devicePixelRatio, maxTouchPoints) via those native switches only. Rendering surfaces (canvas / WebGL / audio / fonts), the TLS ClientHello, and the real browser version are all left untouched, so nothing contradicts the host. 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 a reliable block trigger. It is the lightest identity variation that still passes strict detection; an explicit option always wins over the preset.
import { launch } from "clearcote";
// one coherent metadata identity, rendering/TLS/version left real:
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.
Master seed (int or string). Derives the whole persona (GPU, screen, hardware, fonts, locale) and drives the per-site canvas and WebGL noise. 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.
Examples
These are raw command-line flag sets. For complete Python and Node workflows, see Examples.
A full, coherent Windows identity:
--fingerprint=acme-tenant-7 \
--fingerprint-platform=windows \
--fingerprint-brand=Chrome \
--timezone=America/New_York \
--fingerprint-hardware-concurrency=8A macOS identity with custom GPU strings:
--fingerprint=42 \
--fingerprint-platform=macos \
--fingerprint-gpu-vendor="Apple" \
--fingerprint-gpu-renderer="Apple M2"Pin a location together with its timezone so geolocation and clock agree:
--fingerprint=nyc-1 \
--timezone=America/New_York \
--fingerprint-location=40.7128,-74.0060Import 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. Grab one 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:
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 the seed-derived persona;
// absent fields fall back to --fingerprint, so partial profiles stay coherent.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
Every switch above maps to a readable engine patch. The full map — all 33 diffs and what each one changes — is in docs/PATCHES.md, and the diffs themselves live in patches/.
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.
Related reading