jo-inc/camofox-browser
Stealth headless browser for AI agents — bypass Cloudflare, bot detection, and anti-scraping. Drop-in Puppeteer/Playwright replacement.
README
camofox-browser
Anti-detection browser server for AI agents, powered by Camoufox
Standing on the mighty shoulders of Camoufox - a Firefox fork with fingerprint spoofing at the C++ level.
> Built by the team behind jo, a personal AI agent that runs half on your Mac, half on a dedicated cloud machine just for you -- with zero maintenance needed. Available on macOS, Telegram, WhatsApp, and email. Try the beta free ->
> Pradeep Elankumaran (@pradeep24) — co-founder and technical CEO at Jo.
>
git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser
npm install && npm start
-> http://localhost:9377
---
Why
AI agents need to browse the real web. Playwright gets blocked. Headless Chrome gets fingerprinted. Stealth plugins become the fingerprint.
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.
This project wraps that engine in a REST API built for agents: accessibility snapshots instead of bloated HTML, stable element refs for clicking, and search macros for common sites.
Features
- C++ Anti-Detection - bypasses Google, Cloudflare, and most bot detection
- Element Refs - stable
e1,e2,e3identifiers for reliable interaction - Token-Efficient - accessibility snapshots are ~90% smaller than raw HTML
- Runs on Anything - lazy browser launch + idle shutdown keeps memory at ~40MB when idle. Designed to share a box with the rest of your stack -- Raspberry Pi, $5 VPS, shared infra.
- Session Isolation - separate cookies/storage per user
- Cookie Import - inject Netscape-format cookie files for authenticated browsing
- File Upload - attach files from a configured upload directory without a native OS dialog
- Proxy + GeoIP - route traffic through residential proxies with automatic locale/timezone
- Structured Logging - JSON log lines with request IDs for production observability
- YouTube Transcripts - extract captions from any YouTube video via yt-dlp, no API key needed
- Search Macros -
@google_search,@youtube_search,@amazon_search,@reddit_subreddit, and 10 more - Snapshot Screenshots - include a base64 PNG screenshot alongside the accessibility snapshot
- Large Page Handling - automatic snapshot truncation with offset-based pagination
- Download Capture - capture browser downloads and fetch them via API (optional inline base64)
- DOM Image Extraction - list
src/alt and optionally return inline data URLs - Deploy Anywhere - Docker, Fly.io, Railway
- VNC Interactive Login - log into sites visually via noVNC, export storage state for agent reuse
- OpenAPI Docs - auto-generated spec at
/openapi.jsonand interactive docs at/docs - Structured Extract -
POST /tabs/:tabId/extractwith a JSON Schema that maps properties to snapshot refs viax-ref - Session Tracing - opt-in per-session Playwright trace capture (screenshots + DOM snapshots + network) with API endpoints to list, fetch, and delete trace zips
- Telemetry - automatic anonymized crash/hang telemetry via GitHub Issues. Identifies which sites cause failures and common failure patterns. Private domains are HMAC-hashed, paths/params stripped, tokens/IPs redacted. Opt-out with
CAMOFOX_CRASH_REPORT_ENABLED=false.
Optional Dependencies
| Dependency | Purpose | Install |
|-----------|---------|---------|
| yt-dlp | YouTube transcript extraction (fast path) | pip install yt-dlp or brew install yt-dlp |
The Docker image includes yt-dlp. For local dev, install it for the /youtube/transcript endpoint. Without it, the endpoint falls back to a slower browser-based method.
Quick Start
OpenClaw Plugin
openclaw plugins install @askjo/camofox-browser
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
Standalone
Run from npm:
npx @askjo/camofox-browser
Or from source:
git clone https://github.com/jo-inc/camofox-browser
cd camofox-browser
npm install
npm start # downloads Camoufox on first run (~300MB)
Default port is 9377. See Environment Variables for all options.
Note: the postinstall script unsetsPLAYWRIGHT_SKIP_BROWSER_DOWNLOADfor itself before fetching the Camoufox binary. Without that override, an exportedPLAYWRIGHT_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.
> External Camoufox executable: setCAMOUFOX_EXECUTABLE=/path/to/camoufox-binbeforenpm installand when starting the server to skip the bundled download and launch that executable. Compatibility aliases areCAMOUFOX_EXECUTABLE_PATHandCAMOFOX_EXECUTABLE_PATH. This is useful for NixOS paths such as/nix/store/.../camoufox-bin; the executable must come from a Camoufox bundle that includesproperties.json,version.json, andfontconfig/.
> Air-gapped or custom binary management: preferCAMOUFOX_EXECUTABLEwhen you already have a Camoufox bundle. Otherwise disable the auto-fetch withnpm install --ignore-scripts(skips lifecycle scripts for every dependency -- bluntest option) or, more surgically,npm install --omit=optionalplus a manualnpx camoufox-js fetchstep against your mirror. Note thatPLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm installno longer skips the Camoufox download (the postinstall sanitizes the env locally); use--ignore-scriptsorCAMOUFOX_EXECUTABLEfor that.
Docker
The included Makefile auto-detects your CPU architecture and pre-downloads Camoufox + yt-dlp binaries outside the Docker build, so rebuilds are fast (~30s vs ~3min).
# Build and start (auto-detects arch: aarch64 on M1/M2, x86_64 on Intel)
make up
Stop and remove the container
make down
Force a clean rebuild (e.g. after upgrading VERSION/RELEASE)
make reset
Just download binaries (without building)
make fetch
Override arch or version explicitly
make up ARCH=x86_64
make up VERSION=135.0.1 RELEASE=beta.24
Windows
On Windows, make is not available. Use the included build.ps1 PowerShell script instead:
# Build and start
.\build.ps1 up
Stop and remove the container
.\build.ps1 down
Build image only
.\build.ps1 build
Force a clean rebuild
.\build.ps1 reset
Download binaries only (without building)
.\build.ps1 fetch
Override architecture
.\build.ps1 up -Arch x86_64
.\build.ps1 up -Arch aarch64
Note: PowerShell 7+ (pwsh) is recommended butpowershell.exe(Windows PowerShell 5.1) also works. The script requires Docker Desktop for Windows with the WSL2 backend.
> Line endings: This project includes a.gitattributesfile that forces Unix (LF) line endings for.shfiles. If you've already cloned the repo and getsh: not foundorset: Illegal option -errors duringdocker build, run:
> Get-ChildItem -Recurse *.sh | ForEach-Object { (Get-Content $_) -join "n" + "n" | Set-Content $_ -NoNewline }
This converts shell scripts to LF line endings. Future clones will handle this automatically thanks to .gitattributes.
WARNING: Do not rundocker builddirectly. The Dockerfile uses bind mounts to pull pre-downloaded binaries fromdist/. Always usemake up(ormake fetchthenmake build) -- it downloads the binaries first.
Fly.io
For Fly.io or other remote CI, you'll need a Dockerfile that downloads binaries at build time instead of using bind mounts.
Railway
A railway.toml is included. It uses Dockerfile.ci (which downloads binaries at build time) and maps Railway's PORT env var to CAMOFOX_PORT automatically.
# Install Railway CLI, then:
railway link
railway up
Set secrets via the Railway dashboard or CLI:
railway variables set CAMOFOX_API_KEY="your-generated-key"
Usage
Cookie Import
Import cookies from your browser into Camoufox to skip interactive login on sites like LinkedIn, Amazon, etc.
Setup
1. Generate a secret key:
# macOS / Linux
openssl rand -hex 32
2. Set the environment variable before starting OpenClaw:
export CAMOFOX_API_KEY="your-generated-key"
openclaw start
The same key is used by both the plugin (to authenticate requests) and the server (to verify them). Both run from the same environment -- set it once.
Why an env var? The key is a secret. Plugin config inopenclaw.jsonis stored in plaintext, so secrets don't belong there. SetCAMOFOX_API_KEYin your shell profile, systemd unit, Docker env, or Fly.io secrets.
Cookie import is disabled by default. If CAMOFOX_API_KEY is not set, the server rejects all cookie requests with 403.
3. Export cookies from your browser:
Install a browser extension that exports Netscape-format cookie files (e.g., "cookies.txt" for Chrome/Firefox). Export the cookies for the site you want to authenticate.
4. Place the cookie file:
mkdir -p ~/.camofox/cookies
cp ~/Downloads/linkedin_cookies.txt ~/.camofox/cookies/linkedin.txt
The default directory is ~/.camofox/cookies/. Override with CAMOFOX_COOKIES_DIR.
5. Ask your agent to import them:
Import my LinkedIn cookies from linkedin.txt
The agent calls camofox_import_cookies -> reads the file -> POSTs to the server with the Bearer token -> cookies are injected into the browser session. Subsequent camofox_create_tab calls to linkedin.com will be authenticated.
How it works
~/.camofox/cookies/linkedin.txt (Netscape format, on disk)
|
v
camofox_import_cookies tool (parses file, filters by domain)
|
v POST /sessions/:userId/cookies
| Authorization: Bearer <CAMOFOX_API_KEY>
| Body: { cookies: [Playwright cookie objects] }
v
camofox server (validates, sanitizes, injects)
|
v context.addCookies(...)
|
Camoufox browser session (authenticated browsing)
cookiesPathis resolved relative to the cookies directory -- path traversal outside it is blocked- Max 500 cookies per request, 5MB file size limit
- Cookie objects are sanitized to an allowlist of Playwright fields
Session Persistence
By default, camofox persists each user's cookies and localStorage to ~/.camofox/profiles/. Sessions survive browser restarts -- log in once (via cookies or VNC), and subsequent sessions restore the authenticated state automatically.
~/.camofox/
|-- cookies/ # Bootstrap cookie files (Netscape format)
\-- profiles/ # Persisted session state (auto-managed)
\-- /
\-- storage_state.json
Override the directory with CAMOFOX_PROFILE_DIR or set "profileDir" in the persistence plugin config. To disable persistence, set "persistence": { "enabled": false } in camofox.config.json.
By default, storage state contains cookies and localStorage only. To also persist IndexedDB, set "indexedDB": true in the persistence plugin config. This captures all serializable IndexedDB records—not only authentication data—and may make snapshots significantly larger and checkpoints slower.
Session Tracing
Capture a Playwright trace of every action in a session: page screenshots, DOM snapshots, network requests, and console output. Output is a single .zip file you can open in Playwright's built-in Trace Viewer.
Opt-in per session by passing trace: true when opening the first tab:
curl -X POST http://localhost:9377/tabs \
-H 'Content-Type: application/json' \
-d '{"userId":"agent1","sessionKey":"task1","url":"https://example.com","trace":true}'
The trace is written when the session closes. Close the session to flush it, then list, fetch, and view:
# Close the session to flush the trace
curl -X DELETE http://localhost:9377/sessions/agent1
List trace files
curl http://localhost:9377/sessions/agent1/traces
{"traces":[{"filename":"trace-2026-04-18T04-05-00-...zip","sizeBytes":42810,"createdAt":...}]}
Download (Content-Type: application/zip)
curl http://localhost:9377/sessions/agent1/traces/trace-2026-04-18T04-05-00-abc.zip > session.zip
View it in Playwright's Trace Viewer
npx playwright show-trace session.zip
Delete
curl -X DELETE http://localhost:9377/sessions/agent1/traces/trace-2026-04-18T04-05-00-abc.zip
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).
Tracing cannot be toggled on an existing session. DELETE /sessions/:userId first if you need to change the flag.
Storage defaults to ~/.camofox/traces// and is swept on server startup:
CAMOFOX_TRACES_DIR- base directory (default:~/.camofox/traces)CAMOFOX_TRACES_MAX_BYTES- max size per trace, removed at next startup if exceeded (default: 50MB)CAMOFOX_TRACES_TTL_HOURS- traces older than this are removed at next startup (default: 24)
Standalone server usage
curl -X POST http://localhost:9377/sessions/agent1/cookies \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_CAMOFOX_API_KEY' \
-d '{"cookies":[{"name":"foo","value":"bar","domain":"example.com","path":"/","expires":-1,"httpOnly":false,"secure":false}]}'
Docker / Fly.io / Railway
docker run -p 9377:9377 \
-e CAMOFOX_API_KEY="your-generated-key" \
-v ~/.camofox/cookies:/home/node/.camofox/cookies:ro \
camofox-browser
For Fly.io:
fly secrets set CAMOFOX_API_KEY="your-generated-key"
For Railway:
railway variables set CAMOFOX_API_KEY="your-generated-key"
Proxy + GeoIP
Route all browser traffic through a proxy with automatic locale, timezone, and geolocation derived from the proxy's IP address via Camoufox's built-in GeoIP.
Simple proxy (single endpoint):
export PROXY_HOST=166.88.179.132
export PROXY_PORT=46040
export PROXY_USERNAME=myuser
export PROXY_PASSWORD=mypass
npm start
Backconnect proxy (rotating sticky sessions):
For providers like Decodo, Bright Data, or Oxylabs that offer a single gateway endpoint with session-based sticky IPs:
export PROXY_STRATEGY=backconnect
export PROXY_BACKCONNECT_HOST=gate.provider.com
export PROXY_BACKCONNECT_PORT=7000
export PROXY_USERNAME=myuser
export PROXY_PASSWORD=mypass
npm start
Each browser context gets a unique sticky session, so different users get different IP addresses. Sessions rotate automatically on proxy errors or Google blocks.
Or in Docker:
docker run -p 9377:9377 \
-e PROXY_HOST=166.88.179.132 \
-e PROXY_PORT=46040 \
-e PROXY_USERNAME=myuser \
-e PROXY_PASSWORD=mypass \
camofox-browser
When a proxy is configured:
- All traffic routes through the proxy
- Camoufox's GeoIP automatically sets
locale,timezone, andgeolocationto match the proxy's exit IP - Browser fingerprint (language, timezone, coordinates) is consistent with the proxy location
CAMOFOX_LOCALE and CAMOFOX_TIMEZONE; otherwise Camoufox keeps its own identity defaults.
Telemetry
Browser automation fails in ways that are hard to predict -- Cloudflare challenges, site redesigns breaking selectors, redirect loops, dialog storms, renderer crashes. The scope is wide and the failure modes are diverse. Without telemetry, the only signal is "it didn't work."
Telemetry gives us structured data on which sites fail, how they fail, and how often, so we can prioritize fixes for the patterns that actually affect users. It files GitHub Issues automatically when:
- Uncaught exceptions crash the process
- Event loop stalls exceed 5 seconds (watchdog detection)
- Frustration patterns -- 3+ consecutive failures (timeout, dead context, navigation abort) on the same tab
How it works
Telemetry is sent to a lightweight Cloudflare Worker endpoint at https://camofox-telemetry.askjo.workers.dev. The endpoint holds the GitHub App credentials as environment secrets -- no secrets are shipped in this package.
lib/reporter.js (client, no secrets)
| anonymize -> POST https://camofox-telemetry.askjo.workers.dev/report
v
Cloudflare Worker (holds GitHub App key)
| validate -> rate-limit -> dedup -> create GitHub Issue
v
GitHub Issue created
The endpoint source code is in this repo at workers/crash-reporter/index.ts.
Verification
You don't have to trust us -- verify what the live endpoint is running:
# 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
3. Check the commit matches what CI deployed
https://github.com/jo-inc/camofox-browser/actions/workflows/telemetry-deploy.yml
git log --oneline workers/crash-reporter/index.ts | head -1
If the hashes don't match, the endpoint is running different code than what's in the repo. The deploy workflow (.github/workflows/telemetry-deploy.yml) injects the commit and source hash at deploy time -- every deploy is auditable in GitHub Actions.
Or skip verification entirely: CAMOFOX_CRASH_REPORT_ENABLED=false disables all telemetry, or point to your own endpoint with CAMOFOX_CRASH_REPORT_URL.
Privacy
All reported data goes through paranoid anonymization (lib/reporter.js L28-290) before leaving the process:
- 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 (
/server.js) - Tokens, secrets, API keys -> ``
- IPs, emails, env vars -> redacted
- Docker/Fly machine IDs -> ``
- Tab health -- pure counters (crash count, error count, status code histogram). No page content, no URLs, no user data.
+1 comment instead of a new issue.
# Disable telemetry
export CAMOFOX_CRASH_REPORT_ENABLED=false
Point to your own endpoint (see below)
export CAMOFOX_CRASH_REPORT_URL=https://your-endpoint.example.com/report
Adjust rate limit (default: 10 per hour)
export CAMOFOX_CRASH_REPORT_RATE_LIMIT=5
Self-hosted telemetry endpoint
To file telemetry reports in your own GitHub repo instead of jo-inc/camofox-browser:
1. Create a GitHub App -- Settings -> Developer settings -> GitHub Apps -> New
- Permissions: Repository -> Issues -> Read & Write
- Uncheck Webhook -> Active (not needed)
- Click Generate a key -- downloads a
.pemfile - Install the app on your target repo (Install App -> select repo)
- Note your App ID (number on the app's General page) and Installation ID (from the URL after installing:
github.com/settings/installations/{id})
cd workers/crash-reporter
# Edit wrangler.toml: set account_id to your Cloudflare account ID
npx wrangler deploy
The worker is a single TypeScript file with zero npm dependencies. It also runs on Deno, Bun, or any runtime with the Web Crypto API.
3. Set worker secrets:
cd workers/crash-reporter
echo "YOUR_APP_ID" | npx wrangler secret put GH_APP_ID
echo "YOUR_INSTALL_ID" | npx wrangler secret put GH_INSTALL_ID
# Key must be PKCS#8 DER base64 (not raw PEM)
openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in your-app.pem | \
base64 | tr -d '\n' | npx wrangler secret put GH_PRIVATE_KEY
# File issues in your repo
echo "your-org/your-repo" | npx wrangler secret put GH_REPO
4. Point camofox-browser to your endpoint:
export CAMOFOX_CRASH_REPORT_URL=https://your-worker.your-subdomain.workers.dev/report
5. Verify:
curl https://your-worker.your-subdomain.workers.dev/health
# -> {"status":"ok"}
Structured Logging
All log output is JSON (one object per line) for easy parsing by log aggregators:
{"ts":"2026-02-11T23:45:01.234Z","level":"info","msg":"req","reqId":"a1b2c3d4","method":"POST","path":"/tabs","userId":"agent1"}
{"ts":"2026-02-11T23:45:01.567Z","level":"info","msg":"res","reqId":"a1b2c3d4","status":200,"ms":333}
Health check requests (/health) are excluded from request logging to reduce noise.
Basic Browsing
```bash

