Camofox: Why Your AI Agent Browser Gets Fingerprinted at the C++ Layer — aniketkarneai.com | aniketkarneai.com
Sunday, September 27, 2026 Field notes on autonomous systems ● Amsterdam, NL
daily

Camofox: Why Your AI Agent Browser Gets Fingerprinted at the C++ Layer

Camofox wraps the Camoufox Firefox fork in a REST API for AI agents, doing fingerprint spoofing at the C++ implementation level rather than as a JS shim. It solves the "the stealth plugin is itself the fingerprint" problem with element refs, accessibility snapshots, session tracing via Playwright Trace Viewer, and a privacy-preserving telemetry pipeline. The interesting design choice is what it doesn't do.

Camofox: Why Your AI Agent Browser Gets Fingerprinted at the C++ Layer

There’s a quiet embarrassment in the AI-agent-browser space that nobody on the marketing side wants to talk about. The standard stack — Playwright, headless Chrome, a stealth plugin from npm — gets fingerprinted by every major site that bothers to try. Not because the stack is bad. Because once you bolt a stealth shim onto Chrome, the shim becomes the fingerprint. navigator.webdriver returns false, but a thousand tiny tells (a missing chrome.runtime, an absent Permissions query, a window.chrome object with the wrong shape) give it away. CreepJS, BrowserScan, and FingerprintJS Pro all converge on the same conclusion: anything bolted on top is detectable.

jo-inc/camofox-browser (10,840★, MIT, last pushed 2026-09-09, v1.14.0 on 2026-08-19) takes the opposite bet. It wraps Camoufox — a Firefox fork that patches the browser’s C++ implementation directly — in a REST API built for agents. The fingerprints that Cloudflare, Google, and the long tail of bot-detection vendors actually measure (navigator.hardwareConcurrency, WebGL renderers, AudioContext sample rates, screen geometry, WebRTC local IPs) are spoofed before JavaScript ever sees them. No shims. No wrappers. No tells.

The project is interesting for what it doesn’t do as much as for what it does.

The engineering claim, restated

The README puts it bluntly:

Camoufox patches Firefox at the C++ implementation level - navigator.hardwareConcurrency, WebGL renderers, AudioContext, screen geometry, WebRTC - all spoofed before JavaScript ever sees them. No shims, no wrappers, no tells.

That’s a falsifiable claim, and it’s the right kind of falsifiable. The JavaScript-facing surface (window.navigator, the Permissions API, the WebGLRenderingContext methods) returns what the underlying C++ code synthesizes. A CreepJS-style probe that just reads JS-visible attributes sees a clean, consistent fingerprint. A probe that goes deeper — runtime detection via performance.now() jitter patterns, canvas fingerprint entropy, AudioContext oscillator behavior — also sees clean output, because the C++ side is the original.

This matters for AI agents specifically because the alternative is browser-use’s Playwright-default path, which the BrowserScan and similar services have had years to learn. Camofox is betting that upstream patches survive detector evolution longer than downstream shims do, because detectors can’t reverse-engineer the Firefox source tree from a JS-visible fingerprint they observe at runtime.

The agent-shaped REST API

What’s bolted on top of Camoufox is a deliberately minimal REST API. The POST /tabs endpoint opens a tab; GET /tabs/:id/snapshot returns an accessibility tree with stable e1/e2/e3 element refs; POST /tabs/:id/click accepts a ref and resolves it to the actual DOM node; POST /tabs/:id/navigate accepts a URL or a search macro. There’s an OpenAPI spec at /openapi.json and interactive Swagger UI at /docs. Default port is 9377.

A few details from the API surface that change how you’d design an agent loop:

Accessibility snapshots instead of HTML. The README claims snapshots are “about 90% smaller than raw HTML.” A browser-use agent that pulls e1 [button] Submit, e2 [link] Learn more instead of the full DOM is sending roughly 5–15% of the tokens per page for the same interactive surface. For a 50-step agent loop on a JS-heavy site, that’s not a marginal saving.

Element refs as the interaction primitive. Click and type take {"ref": "e3"}, not a CSS selector or an xpath. The ref is generated server-side when the snapshot is taken, scoped to that snapshot, and guaranteed stable until the next snapshot. The trade-off: refs go stale on any DOM mutation. Camofox’s response to that is “take another snapshot,” which is the right answer for an LLM-driven loop where the model can decide when re-snapshotting is cheaper than retrying with a stale ref.

Search macros for common sites. @google_search, @youtube_search, @amazon_search, @reddit_subreddit, @wikipedia_search, @twitter_search, @yelp_search, @spotify_search, @netflix_search, @linkedin_search, @instagram_search, @tiktok_search, @twitch_search. The Reddit macros return JSON directly, not HTML. This is the kind of plumbing that shows you the maintainers actually run agents against these sites: a generic web search would be a 200-line prompt, a search macro is a single token in the request body.

OpenClaw plugin and the MCP detour

Camofox ships as an OpenClaw plugin under @askjo/camofox-browser. The plugin exposes the same primitives as the REST API but as OpenClaw tools: camofox_create_tab, camofox_snapshot, camofox_click, camofox_type, camofox_navigate, camofox_scroll, camofox_screenshot, camofox_close_tab, camofox_list_tabs, camofox_import_cookies. That’s the canonical install path for users of the OpenClaw harness.

There’s also an MCP server mode — mcp/ in the repo, with a CHANGELOG.md entry from the v1.14.0 cycle (chore: resolve MCP audit findings, dated 2026-08-19). The audit step is telling: MCP integrations are increasingly shipped with security review built in, and this repo’s CI runs that audit before tagging. If you’re building an MCP server for an agent tool in 2026, this is roughly the bar.

The standalone install path is npx @askjo/camofox-browser — the package pulls Camoufox on first run (~300MB binary), starts the server on port 9377, and is ready for curl. For Docker, there’s a Makefile that pre-downloads binaries outside the build so rebuilds take ~30s instead of ~3min. The Dockerfile.ci variant downloads binaries at build time for Fly.io and Railway deploys, where bind mounts aren’t an option.

Telemetry that doesn’t phone home to the user

One of the better pieces of engineering in the project is the crash/hang telemetry pipeline. The README documents it at length, which itself is a signal — most projects hide this in a CONTRIBUTING file.

The trigger conditions are specific:

  • Uncaught exceptions that crash the process
  • Event loop stalls exceeding 5 seconds (watchdog detection)
  • Frustration patterns — 3+ consecutive failures (timeout, dead context, navigation abort) on the same tab

When any of these fire, the client (lib/reporter.js L28-L290) builds a report and POSTs it to https://camofox-telemetry.askjo.workers.dev/report. The Cloudflare Worker holds the GitHub App credentials as environment secrets — no secrets ship in the package. The Worker validates, rate-limits (default 10 reports/hour), deduplicates by stack signature, and creates a GitHub Issue with a +1 comment if the signature is already known.

The anonymization is the part worth reading. URLs go through this pipeline:

  • URLs — well-known public domains (Google, Amazon, Reddit, Cloudflare, etc.) are shown verbatim so we can identify which sites cause problems. Private/unknown domains are replaced with a stable HMAC hash (site-a1b2c3d4) — same hash across reports for correlation, but not reversible to the original domain. Path segments become */*/* (depth only). Query params become ?[3] (count only). No keys, values, or path content is ever included.
  • File paths -> stripped to filename only (<path>/server.js)
  • Tokens, secrets, API keys -> <token>
  • IPs, emails, env vars -> redacted
  • Docker/Fly machine IDs -> <id>
  • Tab health — pure counters (crash count, error count, status code histogram). No page content, no URLs, no user data.

This is the right shape for a privacy-preserving telemetry system. The maintainers want to know which sites cause problems (so they can fix fingerprinting regressions for those sites specifically), but they can’t see the actual URL the agent was on if it’s not in the public-domain allowlist. The HMAC hash is stable across reports so two crashes on the same unknown site correlate, but the hash isn’t reversible without the HMAC key.

There’s also a verification protocol baked in:

# 1. Ask the endpoint what code it's running
curl https://camofox-telemetry.askjo.workers.dev/source
# -> { "commit": "abc1234", "sha256": "e3b0c44...", "source": "https://github.com/..." }
# 2. Compare the sha256 against the source in this repo
sha256sum workers/crash-reporter/index.ts

That’s auditable end-to-end. If the hashes don’t match, the endpoint is running different code than what’s in the repo, and the user can point at CAMOFOX_CRASH_REPORT_URL to their own self-hosted Worker instead. The whole telemetry stack — Worker source, GitHub Action deploy, App credentials, anonymization logic — is in the repo and verifiable.

Where the post-install papercuts live

Camofox’s npm install script has a non-obvious behavior worth knowing about if you run into it:

The postinstall script unsets PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD for itself before fetching the Camoufox binary. Without that override, an exported PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 (common when Playwright is configured to use system Chrome) would silently skip the binary download and crash the server at runtime.

That’s a real production papercut. If you have Playwright configured for PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 (which is a perfectly reasonable thing to do if you’re using system Chrome across multiple tools), Camofox’s npm postinstall will silently do nothing, and the server will crash on first request with a missing-binary error that doesn’t obviously point at the env var. The fix the README documents — npm install --ignore-scripts plus a manual npx camoufox-js fetch against your own mirror, or setting CAMOUFOX_EXECUTABLE=/path/to/camoufox-bin for an existing bundle — is the right escape hatch.

The same README section flags an interesting operational quirk for NixOS users: you have to set CAMOUFOX_EXECUTABLE=/nix/store/.../camoufox-bin and make sure the bundle includes properties.json, version.json, and fontconfig/. The README also documents three aliases — CAMOUFOX_EXECUTABLE, CAMOUFOX_EXECUTABLE_PATH, CAMOFOX_EXECUTABLE_PATH — which is a tell that this is a moving target and the maintainers have been chasing OS-packaging bugs across releases.

Session tracing on Firefox: why not video?

Camofox supports per-session Playwright tracing, opt-in via "trace": true when opening the first tab. The output is a .zip you open in npx playwright show-trace session.zip — page screenshots, DOM snapshots, network requests, console output, all in one file.

The README calls out why traces instead of video:

Camoufox is Firefox-based, and Playwright’s recordVideo is Chromium-only. Traces work on Firefox and give you more than video (network + DOM + console + screenshots).

That’s a real constraint, not a stylistic choice. If you’re debugging an agent loop that mysteriously fails on Firefox-specific rendering (which Firefox’s more conservative WebCompat policy will occasionally produce), Playwright traces give you more diagnostic information than a video would. The trade-off: traces are opt-in per session (tracing cannot be toggled on an existing session; DELETE /sessions/:userId first if you need to change the flag), they default to 50MB max per trace, and they’re swept after 24 hours by default. Defaults that favor privacy and disk pressure over completeness.

The MCP audit, OpenAPI exposure, and the access-key knob

Two security defaults that deserve more attention than the README gives them:

CAMOFOX_ACCESS_KEY, if set, makes all routes (except /health, cookie import, and /stop) require Authorization: Bearer ***. The default is unset, which means the server binds to whatever Node’s default all-interface binding is (all interfaces, IPv4). The bind host is independently controllable via CAMOFOX_BIND_HOST — set it to 127.0.0.1 for loopback-only access.

If you’re deploying this on a shared box, set both. The cookie import endpoint is separately gated by CAMOFOX_API_KEY, but the snapshot and click endpoints are open by default — which means anyone who can reach port 9377 can drive the browser as your agent. That’s fine for local dev, dangerous on a multi-tenant VM.

The OpenAPI spec at /openapi.json and the Swagger UI at /docs are convenient but also leak the surface area to anyone who can reach the server. There’s no auth on /docs by default. Combined with the access-key behavior above, this is an operational choice you’ll want to make explicitly before exposing the server beyond loopback.

Trade-offs and what Camofox doesn’t fix

C++ patches don’t move as fast as detector evolution. Camoufox tracks Firefox ESR plus a fingerprint-spoofing patchset, and the underlying fingerprint vectors are Firefox ESR’s responsibility. If Cloudflare ships a new WebGL extension probe that Firefox ESR doesn’t expose correctly, Camofox inherits the bug. The README’s claim is that the C++ side is harder to detect than shims, which is true, but it also means upgrades are coupled to Camoufox’s release cadence.

The 10,840 stars don’t translate to 10,840 deployments. Star count on a 2024-vintage repo is mostly a measure of how many people bookmarked it on Hacker News at launch. The 1,069 forks and 64 open issues tell you more about active deployment. Of those 64 issues, I’d guess a non-trivial fraction are Cloudflare-vs-Camofox regressions on sites that updated their bot-detection heuristics.

Telemetry is opt-out, not opt-in. CAMOFOX_CRASH_REPORT_ENABLED defaults to on. The README is explicit about this and the anonymization is real, but “this browser engine phones home crash data” is the kind of detail that surprises people reading the README top-down. If you’re running agents against sensitive URLs, set the env var explicitly. (CAMOFOX_CRASH_REPORT_ENABLED=false or point at your own endpoint.)

Search macros are a maintenance liability. 14 macros, each tied to a specific site’s URL shape and DOM conventions. Reddit’s macro returns JSON because Reddit exposes .json suffix URLs; Amazon’s macro presumably scrapes the HTML; LinkedIn’s macro probably hits LinkedIn’s anti-bot wall more often than not. When any of these sites redesigns, that macro breaks. The bet is that the maintainers use these macros daily (via the OpenClaw plugin that runs jo, their personal AI agent), so breakage gets caught fast. That’s a real bet, not a guarantee.

The idle-shutdown 40MB figure is impressive but conditional. Camofox claims ~40MB idle memory because the browser engine doesn’t launch until the first request. If your agent loop opens many concurrent sessions, the memory multiplies. MAX_SESSIONS=50 and MAX_TABS_PER_SESSION=10 are the documented ceilings, but those are guard-rails, not a guarantee that 500 tabs of Chromium-EQR-on-Firefox-EQR fit in 2GB.

What I’d watch in the next release

The v1.14.0 release notes (2026-08-19) flagged an “opt-in interactive desktop browser” mode — CAMOFOX_INTERACTIVE=desktop opens a real local Camoufox window for VNC-style debugging. That’s the right shape for an agent harness whose debugging surface is “watch the agent browse,” not “tail JSON logs.” Combined with playwright show-trace and the existing noVNC login flow, the debugging story is converging on something an engineer can actually sit in front of.

The other thing I’d watch is how Camoufox tracks Firefox’s release cadence. The camoufox-backup-380139564 tag on 2026-09-06 (v152.0.4-beta.30) and the camoufox-backup-373994613 tag on 2026-08-25 (v152.0.4-beta.29) suggest Camofox is consuming Camoufox’s Firefox ESR beta branch every ~12 days. If that cadence holds, fingerprint regressions get patched within two weeks; if it slows, the gap between Camoufox upstream and the fingerprint-detection arms race widens.

The honest framing: Camofox is the best open-source answer I know of to “my AI agent gets blocked on Cloudflare sites within 3 page loads,” and the project’s 2026 trajectory is toward more tooling for debugging agent browser sessions, not toward better evasion. That’s the right trajectory — the evasion arms race is unwinnable in the long run, but the debugging surface is still rough.

References and where to dig further

  • jo-inc/camofox-browser — the project itself
  • camoufox.com — the Firefox fork underneath, with the C++ patchset
  • lib/reporter.js L28-L290 — the anonymization logic, worth reading before deploying with telemetry on
  • workers/crash-reporter/index.ts — the Cloudflare Worker, single TypeScript file, zero npm deps, also runs on Deno and Bun
  • v1.14.0 release notes (2026-08-19) for the CAMOFOX_INTERACTIVE=desktop mode and the MCP audit findings
  • askjo.ai — the personal AI agent that the maintainers run Camofox against daily; the search macros and OpenClaw plugin shapes are downstream of “what does jo actually need to do this week”
Aniket Karne
DevOps & AI Engineer · Amsterdam
Back to all posts
Reader correspondence

Comments

Powered by GitHub Discussions via Giscus. Sign in with GitHub to leave a comment.