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 but powershell.exe (Windows PowerShell 5.1) also works. The script requires Docker Desktop for Windows with the WSL2 backend.
Line endings: This project includes a .gitattributes file that forces Unix (LF) line endings for .sh files. If you’ve already cloned the repo and get sh: not found or set: Illegal option - errors during docker 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 run docker build directly. The Dockerfile uses bind mounts to pull pre-downloaded binaries from dist/. Always use make up (or make fetch then make 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”
Usage
Cookie Import
Import cookies from your browser into Camoufox to skip interactive login on sites like LinkedIn, Amazon, etc.
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.json is stored in plaintext, so secrets don’t belong there. Set CAMOFOX_API_KEY in 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.
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.
mkdir -p ~/.camofox/cookies cp ~/Downloads/linkedin_cookies.txt ~/.camofox/cookies/linkedin.txt
The default directory is ~/.camofox/cookies/. Override with CAMOFOX_COOKIES_DIR.
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 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) \-- <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.
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.
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:
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.
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.
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:
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:
Each report includes the failure type, stack trace, tab health counters (HTTP status histogram, console errors, request failures, redirect depth), and the target URL – all anonymized.
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.
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.
All reported data goes through paranoid anonymization (lib/reporter.js L28-290) before leaving the process:
Duplicate issues are detected by stack signature and get a +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:
Create a GitHub App – Settings -> Developer settings -> GitHub Apps -> New
Deploy the endpoint – clone this repo and deploy the worker:
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.
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
- Point camofox-browser to your endpoint: export CAMOFOX_CRASH_REPORT_URL=https://your-worker.your-subdomain.workers.dev/report
- 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.
# 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"}'
API
Tab Lifecycle
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
Page Interaction
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
YouTube Transcript
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] [music] We're no strangers to love [music]\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
Sessions
Method Endpoint Description
POST
/sessions/:userId/cookies
Add cookies to a user session (Playwright cookie objects)
GET
/sessions/:userId/storage_state
Export persisted browser storage (VNC plugin)
DELETE
/sessions/:userId/storage_state
Reset the live session and delete its persisted browser storage (persistence plugin)
Search Macros
@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):
Browser behavior can be tuned in camofox.config.json: