Anti-detection browser server for AI agents, powered by Camoufox
<a href="https://github.com/jo-inc/camofox-browser/raw/v1.11.2/LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
<a href="https://github.com/jo-inc/camofox-browser/stargazers"><img src="https://img.shields.io/github/stars/jo-inc/camofox-browser" alt="GitHub stars" /></a>
<a href="https://www.npmjs.com/package/camofox-browser"><img src="https://img.shields.io/npm/v/camofox-browser" alt="npm version" /></a>
<a href="https://github.com/jo-inc/camofox-browser/commits"><img src="https://img.shields.io/github/last-commit/jo-inc/camofox-browser" alt="GitHub last commit" /></a>
Standing on the mighty shoulders of <a href="https://camoufox.com">Camoufox</a> - 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 ->
git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser
npm install && npm start
# -> http://localhost:9377
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.
e1, e2, e3 identifiers for reliable interaction@google_search, @youtube_search, @amazon_search, @reddit_subreddit, and 10 more<img> src/alt and optionally return inline data URLs/openapi.json and interactive docs at /docsPOST /tabs/:tabId/extract with a JSON Schema that maps properties to snapshot refs via x-refCAMOFOX_CRASH_REPORT_ENABLED=false.| 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.
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
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 unsets
PLAYWRIGHT_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: set
CAMOUFOX_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: prefer
CAMOUFOX_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.
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
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:powershell 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 run
docker builddirectly. The Dockerfile uses bind mounts to pull pre-downloaded binaries fromdist/. Always usemake up(ormake fetchthenmake build) -- it downloads the binaries first.
For Fly.io or other remote CI, you'll need a Dockerfile that downloads binaries at build time instead of using bind mounts.
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"
Import cookies from your browser into Camoufox to skip interactive login on sites like LinkedIn, Amazon, etc.
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 in
openclaw.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_KEYis 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.
~/.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)
cookiesPath is resolved relative to the cookies directory -- path traversal outside it is blockedBy 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)
\-- <hashed-userId>/
\-- 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.
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/<hashed-userId>/ and is swept on server startup:
CAMOFOX_TRACES_DIR - base directory (default: ~/.camofox/traces)CAMOFOX_TRACES_MAX_BYTES - max size per trace,$ claude mcp add camofox-browser \
-- python -m otcore.mcp_server <graph>