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.
The same engine behind Jo — an AI assistant that doesn't need you to babysit it. Runs half on your Mac, half on a dedicated cloud machine that only you use. Available on macOS, Telegram, and WhatsApp. Try the beta free →
git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser
npm install && npm start
# → http://localhost:9377AI 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.
- 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 Railway infra.
- Session Isolation - separate cookies/storage per user
- Cookie Import - inject Netscape-format cookie files for authenticated browsing
- 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
<img>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
| 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-browserTools: camofox_create_tab · camofox_snapshot · camofox_click · camofox_type · camofox_navigate · camofox_scroll · camofox_screenshot · camofox_close_tab · camofox_list_tabs · camofox_import_cookies
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.
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
⚠️ 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.
railway.toml is included. For Fly.io or other remote CI, you'll need a Dockerfile that downloads binaries at build time instead of using bind mounts — see jo-browser for an example.
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 322. Set the environment variable before starting OpenClaw:
export CAMOFOX_API_KEY="your-generated-key"
openclaw startThe 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.txtThe 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)
│
▼
camofox_import_cookies tool (parses file, filters by domain)
│
▼ POST /sessions/:userId/cookies
│ Authorization: Bearer <CAMOFOX_API_KEY>
│ Body: { cookies: [Playwright cookie objects] }
▼
camofox server (validates, sanitizes, injects)
│
▼ 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
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)
└── <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.
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 run -p 9377:9377 \
-e CAMOFOX_API_KEY="your-generated-key" \
-v ~/.camofox/cookies:/home/node/.camofox/cookies:ro \
camofox-browserFor Fly.io:
fly secrets set CAMOFOX_API_KEY="your-generated-key"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 startBackconnect 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 startEach 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-browserWhen 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
- Without a proxy, defaults to
en-US,America/Los_Angeles, San Francisco coordinates
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.
# Create a tab
curl -X POST http://localhost:9377/tabs \
-H 'Content-Type: application/json' \
-d '{"userId": "agent1", "sessionKey": "task1", "url": "https://example.com"}'
# Get accessibility snapshot with element refs
curl "http://localhost:9377/tabs/TAB_ID/snapshot?userId=agent1"
# → { "snapshot": "[button e1] Submit [link e2] Learn more", ... }
# Click by ref
curl -X POST http://localhost:9377/tabs/TAB_ID/click \
-H 'Content-Type: application/json' \
-d '{"userId": "agent1", "ref": "e1"}'
# Type into an element
curl -X POST http://localhost:9377/tabs/TAB_ID/type \
-H 'Content-Type: application/json' \
-d '{"userId": "agent1", "ref": "e2", "text": "hello", "pressEnter": true}'
# Navigate with a search macro
curl -X POST http://localhost:9377/tabs/TAB_ID/navigate \
-H 'Content-Type: application/json' \
-d '{"userId": "agent1", "macro": "@google_search", "query": "best coffee beans"}'| Method | Endpoint | Description |
|---|---|---|
POST |
/tabs |
Create tab with initial URL |
GET |
/tabs?userId=X |
List open tabs |
GET |
/tabs/:id/stats |
Tab stats (tool calls, visited URLs) |
DELETE |
/tabs/:id |
Close tab |
DELETE |
/tabs/group/:groupId |
Close all tabs in a group |
DELETE |
/sessions/:userId |
Close all tabs for a user |
| Method | Endpoint | Description |
|---|---|---|
GET |
/tabs/:id/snapshot |
Accessibility snapshot with element refs. Query params: includeScreenshot=true (add base64 PNG), offset=N (paginate large snapshots) |
POST |
/tabs/:id/click |
Click element by ref or CSS selector |
POST |
/tabs/:id/type |
Type text into element |
POST |
/tabs/:id/press |
Press a keyboard key |
POST |
/tabs/:id/scroll |
Scroll page (up/down/left/right) |
POST |
/tabs/:id/navigate |
Navigate to URL or search macro |
POST |
/tabs/:id/wait |
Wait for selector or timeout |
GET |
/tabs/:id/links |
Extract all links on page |
GET |
/tabs/:id/images |
List <img> elements. Query params: includeData=true (return inline data URLs), maxBytes=N, limit=N |
GET |
/tabs/:id/downloads |
List captured downloads. Query params: includeData=true (base64 file data), consume=true (clear after read), maxBytes=N |
GET |
/tabs/:id/screenshot |
Take screenshot |
POST |
/tabs/:id/back |
Go back |
POST |
/tabs/:id/forward |
Go forward |
POST |
/tabs/:id/refresh |
Refresh page |
| Method | Endpoint | Description |
|---|---|---|
POST |
/youtube/transcript |
Extract captions from a YouTube video |
curl -X POST http://localhost:9377/youtube/transcript \
-H 'Content-Type: application/json' \
-d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "languages": ["en"]}'
# → { "status": "ok", "transcript": "[00:18] ♪ We're no strangers to love ♪\n...", "video_title": "...", "total_words": 548 }Uses yt-dlp when available (fast, no browser needed). Falls back to a browser-based intercept method if yt-dlp is not installed — this is slower and less reliable due to YouTube ad pre-rolls.
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
POST |
/start |
Start browser engine |
POST |
/stop |
Stop browser engine |
| Method | Endpoint | Description |
|---|---|---|
POST |
/sessions/:userId/cookies |
Add cookies to a user session (Playwright cookie objects) |
GET |
/sessions/:userId/storage_state |
Export cookies + localStorage (VNC plugin) |
@google_search · @youtube_search · @amazon_search · @reddit_search · @reddit_subreddit · @wikipedia_search · @twitter_search · @yelp_search · @spotify_search · @netflix_search · @linkedin_search · @instagram_search · @tiktok_search · @twitch_search
Reddit macros return JSON directly (no HTML parsing needed):
@reddit_search- search all of Reddit, returns JSON with 25 results@reddit_subreddit- browse a subreddit (e.g., query"programming"→/r/programming.json)
| Variable | Description | Default |
|---|---|---|
CAMOFOX_PORT |
Server port | 9377 |
PORT |
Server port (fallback, for platforms like Fly.io) | 9377 |
CAMOFOX_API_KEY |
Enable cookie import endpoint (disabled if unset) | - |
CAMOFOX_ADMIN_KEY |
Required for POST /stop |
- |
CAMOFOX_COOKIES_DIR |
Directory for cookie files | ~/.camofox/cookies |
CAMOFOX_PROFILE_DIR |
Directory for persisted session profiles | ~/.camofox/profiles |
MAX_SESSIONS |
Max concurrent browser sessions | 50 |
MAX_TABS_PER_SESSION |
Max tabs per session | 10 |
SESSION_TIMEOUT_MS |
Session inactivity timeout | 1800000 (30min) |
BROWSER_IDLE_TIMEOUT_MS |
Kill browser when idle (0 = never) | 300000 (5min) |
HANDLER_TIMEOUT_MS |
Max time for any handler | 30000 (30s) |
MAX_CONCURRENT_PER_USER |
Concurrent request cap per user | 3 |
MAX_OLD_SPACE_SIZE |
Node.js V8 heap limit (MB) | 128 |
PROXY_STRATEGY |
Proxy mode: backconnect (rotating sticky sessions) or blank (single endpoint) |
- |
PROXY_PROVIDER |
Provider name for session format (e.g. decodo) |
decodo |
PROXY_HOST |
Proxy hostname or IP (simple mode) | - |
PROXY_PORT |
Proxy port (simple mode) | - |
PROXY_USERNAME |
Proxy auth username | - |
PROXY_PASSWORD |
Proxy auth password | - |
PROXY_BACKCONNECT_HOST |
Backconnect gateway hostname | - |
PROXY_BACKCONNECT_PORT |
Backconnect gateway port | 7000 |
PROXY_COUNTRY |
Target country for proxy geo-targeting | - |
PROXY_STATE |
Target state/region for proxy geo-targeting | - |
TAB_INACTIVITY_MS |
Close tabs idle longer than this | 300000 (5min) |
ENABLE_VNC |
Enable VNC plugin for interactive browser access (1) |
- |
VNC_PASSWORD |
Password for VNC access (recommended in production) | - |
NOVNC_PORT |
noVNC web UI port | 6080 |
Browser Instance (Camoufox)
└── User Session (BrowserContext) - isolated cookies/storage
├── Tab Group (sessionKey: "conv1")
│ ├── Tab (google.com)
│ └── Tab (github.com)
└── Tab Group (sessionKey: "conv2")
└── Tab (amazon.com)
Sessions auto-expire after 30 minutes of inactivity. The browser itself shuts down after 5 minutes with no active sessions, and relaunches on the next request.
When a session's tab limit is reached, the oldest/least-used tab is automatically recycled instead of returning an error — so long-running agent sessions don't hit dead ends.
npm test # all tests
npm run test:e2e # e2e tests only
npm run test:live # live site tests (Google, macros)
npm run test:debug # with server outputnpm install @askjo/camofox-browser- Camoufox - Firefox-based browser with C++ anti-detection
- Donate to Camoufox's original creator daijro
- OpenClaw - Open-source AI agent framework
Sketchy people are doing sketchy things with crypto tokens named "Camofox" now that this project is getting attention. Camofox is not a crypto project and will never be one. Any token, coin, or NFT using the Camofox name has nothing to do with us.
MIT