Skip to content

Camoufox and the patched-Firefox approach to fingerprint resistance

· 19 min read
Copyright: MIT
The word Camoufox in monospace with an orange underline on a black background

Almost every anti-detect browser you can name is a Chromium fork. Multilogin, GoLogin, Dolphin, the undetected-chromedriver family, the nodriver line, all of them start from Chrome’s source or Chrome’s binary and work backwards from there. The reasons are obvious. Chrome owns the market, so a spoofed Chrome fingerprint hides in the largest crowd, and the Chrome DevTools Protocol gives you a ready-made automation channel. Camoufox does the opposite. It starts from Firefox, throws away CDP entirely, and drives the browser through Juggler, the custom protocol Playwright already ships for Firefox. That is a strange bet to make in 2026, and it is worth understanding why someone made it.

The short version is that the thing that makes Chrome easy to automate is the same thing that makes it easy to detect. CDP is loud. It announces itself in the page scope, it leaves residue an instrumentation check can find, and the V8 patch war over Runtime.enable has been running for years. Firefox’s automation surface is quieter, the engine is a different codebase with different tells, and patching it at the C++ level means the spoofed values never pass through a JavaScript wrapper that a detector can stringify. This post is a single-tool deep dive into how Camoufox actually does that, what it patches, and the specific ways the Firefox base both helps and hurts.

The sections below start with what Camoufox is and what it is built from. Then the core mechanism: how a config value travels from a Python dictionary into a native getter inside the engine, through a singleton the project calls MaskConfig. After that, Juggler and the deliberate absence of CDP, the case for Firefox as a base, the leaks the patches close and the ones they cannot, and a closing read on where the approach sits now that Mozilla has pulled CDP out of Firefox for good.

What Camoufox is

Camoufox is an open-source browser built on Firefox, distributed under the MPL-2.0 license, created by the developer who goes by daijro and now maintained by a group called Clover Labs after the original author stepped back. It is not a separate engine. It is the Firefox source tree with a stack of patches applied, compiled into a debloated binary that the documentation describes as a build stripped of telemetry, background services, and UI overhead running at roughly 200MB. The releases track upstream Firefox closely; recent builds have ridden Firefox 135 and the 146-series, with the version number mirroring the Firefox base it was cut from.

The pitch is fingerprint injection without JavaScript injection. Every property a detection script might probe, the navigator block, WebGL parameters, screen and window geometry, font metrics, WebRTC addresses, audio sample rates, gets intercepted before it reaches the page, and the interception happens in compiled C++ rather than in a script the browser runs at document start. When a page reads navigator.hardwareConcurrency or renders into a WebGL context and reads back the renderer string, the value it gets is the spoofed one, and there is no wrapper function sitting in the JavaScript realm for the page to find. That distinction is the entire reason the project exists, and the engine-level spoofing post covers the general argument for why patching the C++ beats patching the prototype. Here the focus is narrower: how Camoufox specifically wires that up.

Around the core injection sit the usual debloating and privacy patches borrowed from elsewhere in the Firefox-fork world: configuration from LibreWolf, uBlock Origin with custom filter lists, and tuning profiles the project names PeskyFox and FastFox. There is a C++ port of a human-cursor algorithm, originally from riflosnake’s HumanCursor project, rewritten for distance-aware mouse trajectories. Headless mode is patched so it presents the same surface as a windowed instance, with a virtual-display fallback in the Python library if headless ever leaks. None of those are the interesting part. The interesting part is the data path.

How a config value reaches a native getter

The mechanism that makes Camoufox what it is can be drawn as a pipeline. A user passes a config dictionary in Python. That dictionary has to cross the process boundary into a freshly launched browser, get parsed somewhere inside the engine, and be available to the C++ code that answers a property read on the hot path, all without the browser ever exposing a JavaScript hook that betrays the spoof. Camoufox solves this with a singleton it calls MaskConfig and an unusual transport: environment variables.

config dict → spoofed native read Python: launch_options() merge user + BrowserForge validate vs properties.json reject unknown keys CAMOU_CONFIG_1, _2, ... JSON chunked into env vars MaskConfig (C++ singleton) reassemble + parse on startup nsGlobalWindowInner / nsScreen getter calls MaskConfig::GetString() page reads a value that is native all the way down *The config never touches the JavaScript realm. It rides environment variables into a C++ singleton, and native getters query that singleton on each read.*

Walk it stage by stage. In the Python layer, launch_options() takes whatever keys the user supplied and merges them with a generated fingerprint for everything left unset. Those keys are then validated against a schema the project keeps in properties.json, which lists every property name Camoufox knows how to spoof. A key that is not in the schema gets rejected rather than silently ignored, which matters because a typo in a property name is otherwise the kind of bug you only notice when a detector catches the real value leaking through.

Then the unusual step. The merged config is serialized to JSON and split across a numbered series of environment variables, CAMOU_CONFIG_1, CAMOU_CONFIG_2, and so on, chunked because a single environment variable or command-line argument has a length limit a full fingerprint can blow past. The browser launches with those variables set. Inside the engine, on startup, the MaskConfig singleton reads every CAMOU_CONFIG_* variable in order, concatenates them back into one string, and parses the JSON once. From that point the config lives in native memory, and the runtime hooks that answer property reads query it through a call the project exposes as MaskConfig::GetString(). The interception points named in the project’s own architecture docs include nsGlobalWindowInner for window and navigator surfaces, nsScreen for screen geometry, and ClientWebGLContext for the WebGL parameter and renderer strings.

The reason environment variables matter is subtle and worth stating plainly. A common alternative is to inject a JavaScript bundle at document start that defines the spoofed properties. That works, and it is what most stealth tooling does, but it puts the configuration inside the same JavaScript realm the detection script runs in. The script can sometimes find the bundle, or find the side effects of the bundle, or simply notice that a property getter stringifies to something other than [native code]. Camoufox’s transport keeps the config out of that realm completely. There is no script to find because the values are baked into the compiled getters and fed from process memory the page cannot read.

The property surface

The schema in properties.json is the most concrete thing to point at when describing what Camoufox can actually spoof, because the key names are exact and the list is not marketing copy. The navigator block includes navigator.userAgent, navigator.appVersion, navigator.oscpu, navigator.platform, navigator.hardwareConcurrency, navigator.language, navigator.languages, navigator.productSub, navigator.buildID, and navigator.maxTouchPoints, among others. Screen and window geometry covers screen.width, screen.height, screen.availWidth, screen.colorDepth, window.innerWidth, window.outerHeight, window.screenX, and window.devicePixelRatio. There are header overrides keyed as headers.User-Agent, headers.Accept-Language, and headers.Accept-Encoding. WebRTC exposes webrtc:ipv4, webrtc:ipv6, webrtc:localipv4, and webrtc:localipv6. Geolocation and locale carry geolocation:latitude, geolocation:longitude, geolocation:accuracy, timezone, and a locale: family. WebGL is keyed as webGl:renderer and webGl:vendor. Audio is AudioContext:sampleRate. Canvas anti-fingerprinting arrived in a beta in the 133 series and is driven by a canvas:seed key plus anti-aliasing offset parameters.

Two things stand out from that list. First, the header keys live in the same config as the JavaScript-visible properties, which means the User-Agent that goes out on the wire and the navigator.userAgent the page reads come from one source of truth. Mismatches between those two are a classic detection signal, and keeping them in a single dictionary is how you avoid shipping a fingerprint where the HTTP layer and the DOM layer disagree. Second, the WebRTC keys are spoofed at the protocol level rather than by blocking WebRTC, which preserves the appearance of a normal browser while controlling the IP the page would otherwise learn.

The chunking across numbered environment variables is a small detail that tells you something about the constraints. A populated fingerprint is not a handful of fields. It is the full navigator block, every screen and window dimension, a WebGL renderer string and its parameter set, the font list, header values, and the rest, which serializes to a JSON blob large enough that a single environment variable or a command-line argument would truncate it on at least one platform. Splitting it into CAMOU_CONFIG_1 through CAMOU_CONFIG_N and reassembling inside the engine is the workaround. It also means the transport is opaque to a quick ps or a process-listing check that scans command-line arguments for automation flags, since the payload is in the environment block rather than the argument vector.

one fingerprint JSON, chunked to fit the env block CAMOU_CONFIG_1 CAMOU_CONFIG_2 ... _N MaskConfig: concat + parse once native getters query it per read, no script in the page realm *The fingerprint is split to clear the environment-block size limit, then reassembled once inside the engine; the page realm never sees any of it.*

What is not on the list is as informative as what is. The Camoufox docs are explicit that the browser does not fully support injecting Chromium fingerprints, because some web application firewalls test for SpiderMonkey-specific engine behavior, and you cannot make SpiderMonkey behave like V8 by editing a config. That is a hard limit of the approach, and the next two sections are really about its consequences.

Juggler, and the absence of CDP

To automate a browser you need a control channel: a way for your script to open pages, click, type, evaluate JavaScript, and intercept network requests. In Chromium that channel is the Chrome DevTools Protocol. In Firefox, Playwright uses something else. When Microsoft built Playwright’s Firefox support, Firefox did not have a usable CDP implementation, so the Playwright team patched Firefox to add their own protocol, named Juggler, shipped as an XPCOM component compiled into the Firefox patches that Playwright maintains in its tree. Camoufox inherits that, because Camoufox is driven through Playwright, and then patches Juggler further.

Juggler is a remote-debugging pipe registered as an XPCOM service. It is enabled with a --juggler-pipe command-line flag, handled through nsICommandLineHandler, and it observes Firefox lifecycle events through nsIObserver. The protocol is message-based and routed through a component the architecture docs call the Dispatcher, with a TargetRegistry tracking pages and contexts and a NetworkObserver watching request events. Per-frame communication runs over JSWindowActors, the JugglerFrameParent and JugglerFrameChild pair, which is the modern Firefox mechanism for parent-process code to talk to content-process frames. The important structural fact is that the automation code runs in privileged chrome scope, in the browser process, separate from the content processes where page JavaScript executes. Configuration reaches the content side through Firefox’s SharedData channel rather than through anything the page can enumerate.

That separation is the whole point, and it is where Camoufox’s Juggler patches earn their keep. The project gives Playwright its own isolated copy of the page to read and edit. From Playwright’s side everything looks normal, but the real page the user sees is unaffected by those reads and edits, so a detection script cannot catch automation by watching for the side effects of it: hijacked getters firing, mutation observers being attached, listeners appearing on elements. Input is the other half. Juggler, as patched, sends its synthetic clicks and keystrokes through Firefox’s original user-input handlers, so they are processed on the same code path as a real person’s mouse and keyboard rather than through a separate synthetic-event route a page could distinguish.

CDP (Chromium) Juggler (Camoufox) automation protocol surfaces in page scope Runtime.enable, CDP residue page can probe for it privileged browser process isolated copy of the page real page unaffected by reads input via native handlers the page sees a browser, not a driver *CDP exposes automation in the same scope the page runs in; Juggler keeps it in the privileged process and hands the page an untouched copy.*

It is worth being precise about why CDP is the thing being avoided, because the contrast is the argument. CDP’s design makes no effort to hide that it is an automation protocol, and parts of its functionality are reachable from the page scope, which is what turned Runtime.enable into a years-long detection vector and patch war on the Chromium side. Crawlex has written that story at length in the Runtime.enable leak post and on CDP as a detection vector. Camoufox sidesteps the entire category by not speaking CDP at all. There is no CDP endpoint to leak because the protocol is not compiled in the way it is used; the control plane is Juggler, living in the privileged process, and the page never gets a handle to it.

Why Firefox is an interesting base

Choosing Firefox costs you the crowd. The spoofed fingerprint sits in a smaller population, and any detector that simply weights Firefox traffic as more suspicious starts you at a disadvantage. So the choice has to buy something back, and it does, in three ways that are specific to this engine.

The first is the SpiderMonkey tells cut both ways. A WAF that tests for SpiderMonkey-specific behavior can catch a Firefox base pretending to be Chrome, which is the limitation already noted. But the same engine difference means a Firefox base does not carry any of Chrome’s tells, and there are a lot of those. The whole literature of headless-Chrome detection, the HeadlessChrome user-agent token, the navigator.webdriver flag wired into Blink, the CDP residue, the V8-specific stack-trace formatting that instrumentation checks key on, none of it applies to a SpiderMonkey browser. You are not fighting Chrome’s detection surface because you are not Chrome. You inherit Firefox’s surface instead, which is smaller and less picked-over because fewer bot operators target it.

The second is the automation channel is structurally quieter. CDP was retrofitted into Chromium’s existing DevTools backend, and that backend is reachable from page context in ways that keep leaking. Juggler was built specifically for out-of-process automation, with the control plane in privileged chrome scope from the start. The architecture docs describe Juggler as operating on a lower level than CDP, which makes it less prone to JavaScript leaks, and that is not a tuning claim, it follows from where the protocol lives. The cross-realm and cross-worker consistency problems that dog JavaScript shims also bite less here, because the spoofed values come from MaskConfig in native code rather than from a script that has to be re-run in every realm and every worker to stay consistent.

The third is the source is readable. Firefox is MPL-2.0, Camoufox is MPL-2.0, and the patches are public enough that you can read what is changed rather than infer it from observed behavior. That is not a stealth advantage in itself, the opposite if anything, since a detector author can read the same patches. But it is the reason a post like this one can describe MaskConfig and the CAMOU_CONFIG_* transport by name instead of guessing. The closed Chromium anti-detect browsers covered in the anti-detect browsers compared post hide their patches; Camoufox does not, which makes it the best available specimen for understanding how engine-level injection works at all.

What the patches close, and what they cannot

The leaks Camoufox closes are the ones a Firefox automation stack would otherwise show. navigator.webdriver is forced back to the value a normal browser reports rather than the true that automated Firefox sets. The CDP-style residue that a hybrid stack might leave is absent because there is no CDP. Headless mode is patched to match a windowed window, with the virtual-display fallback for the cases where headless still differs. The font surface is handled by bundling Windows, Mac, and Linux system fonts with the build and selecting the set that matches the spoofed User-Agent, so a Linux host claiming to be Windows actually has the Windows font metrics available to render with, plus a small randomized offset on letter spacing to perturb text-measurement fingerprinting. The fingerprint values themselves come from BrowserForge, daijro’s own generator, which models the statistical distribution of real device characteristics so that a generated identity is internally coherent and plausibly common rather than a random and therefore rare combination.

The consistency point deserves a sentence on its own because it is where naive spoofing dies. A fingerprint is caught not by any single odd value but by combinations that never co-occur in real traffic: a macOS User-Agent with a Linux WebGL renderer, a 4K screen with a phone’s touch-point count, a timezone that contradicts the geolocation. BrowserForge exists to keep the generated identity on the manifold of real devices, sampling, in the project’s own example, a Linux user about five percent of the time and, within that slice, a particular screen resolution and GPU at the frequencies they actually appear. That is the same problem the stealth-plugins-lose post describes from the Chrome side, and a native injection layer does not solve it for free. It only gives you a clean surface to put a consistent fingerprint on.

What it cannot do is change the engine. The SpiderMonkey limitation is not a missing feature, it is a wall. JavaScript engine behavior, the exact way SpiderMonkey handles certain operations and error formats and timing, is observable from script and cannot be made to match V8 by editing native getters, because the getters are not where the difference lives. A detector that fingerprints the engine itself, as some firewalls do, sees SpiderMonkey no matter what the navigator block claims. That is the honest ceiling on a Firefox-based browser pretending to be anything other than Firefox, and Camoufox’s own documentation states it plainly rather than papering over it.

There is also the readability double-edge. Because the patches are public, a sufficiently motivated detector can study them and build checks against the specific way Camoufox spoofs, the same dynamic that eventually breaks every public stealth tool described in the nodriver and undetected-chromedriver post. The native layer raises the cost of detection, it does not remove it. What it buys is time, and on the four-to-six-year cycle that governs this whole field, time is the only thing any of these tools are really selling.

Where this leaves the approach

The timing of Camoufox’s bet looks better in hindsight than it might have at the start. Mozilla deprecated its experimental CDP support in Firefox 129 in 2024, flipping the remote.active-protocols default so CDP was off, and then removed the implementation entirely, with Firefox 141 the first release where CDP simply is not there. Firefox’s CDP was always partial anyway, an experimental subset covering 82 of the protocol’s APIs, and the ecosystem moved to WebDriver BiDi: Puppeteer from version 23, Selenium from 4.29.0 in February 2025, Cypress, all migrated off Firefox CDP. A tool that had built its Firefox stealth on CDP would have spent the last two years rewriting. Camoufox never depended on CDP, so none of that touched it. The control plane was Juggler the whole time, and Juggler is Playwright’s to maintain.

The deeper point is that Camoufox is the cleanest public demonstration that the JavaScript layer is the wrong place to fight fingerprint detection, and that the right place is the engine, paid for in compile time and a maintenance burden of rebasing patches onto each new Firefox. The cost of that approach is real. You track upstream Firefox forever, you eat the smaller-crowd penalty, and you hit the SpiderMonkey wall the moment a target fingerprints the engine rather than the navigator block. What you get back is a control plane the page cannot see and a set of spoofed values that are native all the way down, with no wrapper to stringify and no shim to catch in a fresh realm. Whether that trade is worth making depends entirely on what you are up against, and for the growing set of detectors that fingerprint the engine itself, a Firefox honest enough to admit it is Firefox is exactly the wrong tool. For everything below that bar, it is one of the few public browsers that gets the layering right.


Sources & further reading

  • Camoufox (2026), Introduction — official docs for the patched-Firefox build, the C++ injection claim, and the debloated 200MB footprint.
  • Camoufox (2026), Stealth Overview — how the Juggler patches isolate Playwright’s page copy and route input through native handlers.
  • Camoufox (2026), Fingerprint Injection — the config-dictionary interface and the property categories Camoufox can spoof.
  • daijro / Clover Labs (2026), camoufox README — feature list, leak fixes, font bundling, HumanCursor port, and the SpiderMonkey limitation, MPL-2.0.
  • DeepWiki (2026), daijro/camoufox architecture — the MaskConfig singleton, the CAMOU_CONFIG_* environment-variable transport, and the nsGlobalWindowInner / nsScreen / ClientWebGLContext interception points.
  • DeepWiki (2026), Camoufox Juggler System — Juggler as an XPCOM service, the Dispatcher, JSWindowActors, and the privileged-scope separation from page JavaScript.
  • daijro (2026), BrowserForge — the fingerprint generator Camoufox uses to keep spoofed identities statistically coherent.
  • Mozilla / Firefox DevEx (2024), Deprecating CDP Support in Firefox — the May 2024 deprecation, the remote.active-protocols preference, and the 82-API subset.
  • Mozilla / Firefox DevEx (2025), CDP Retirement in Firefox — the full removal of CDP from Firefox and the Firefox 141 cutoff.
  • Selenium (2025), Removing Chrome DevTools Support for Firefox — Selenium 4.29.0, February 2025, dropping Firefox CDP for WebDriver BiDi.
  • Microsoft (2026), Playwright Firefox Juggler patches — the upstream Juggler protocol source Camoufox builds on.

Further reading