Technical Documentation: jo-inc/camofox-browser
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
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:9377Why
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-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
Standalone
Run from npm:
npx @askjo/camofox-browserOr 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.
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.24Windows
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 aarch64Note: 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:powershellGet-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.
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 upSet 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 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.
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)
\-- <hashed-userId>/
\-- storage_state.jsonOverride 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.zipWhy 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, 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-browserFor 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 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, Camofox does not claim a geolocation or infer one from the host IP. To use a fixed direct-session identity, set both 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
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.
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 createdThe 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 -1If 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 ->
<token> - 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.
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=5Self-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
- 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})
Deploy the endpoint -- clone this repo and deploy the worker:
bashcd workers/crash-reporter # Edit wrangler.toml: set account_id to your Cloudflare account ID npx wrangler deployThe worker is a single TypeScript file with zero npm dependencies. It also runs on Deno, Bun, or any runtime with the Web Crypto API.
Set worker secrets:
bashcd 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_REPOPoint camofox-browser to your endpoint:
bashexport CAMOFOX_CRASH_REPORT_URL=https://your-worker.your-subdomain.workers.dev/reportVerify:
bashcurl 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
# 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 `` 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.
Server
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
POST |
/start |
Start browser engine |
POST |
/stop |
Stop browser engine |
Sessions
| Method | Endpoint | Description |
|---|---|---|
GET |
/tabs/:tabId/downloads |
List captured browser downloads and saved current-resource PDFs |
POST |
/tabs/:tabId/fetch-current-resource |
Save the current inline PDF with its browser-session authentication as a download artifact |
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):
@reddit_search- search all of Reddit, returns JSON with 25 results@reddit_subreddit- browse a subreddit (e.g., query"programming"->/r/programming.json)
Browser Configuration
Browser behavior can be tuned in camofox.config.json:
{
"newPageTimeoutMs": 10000
}newPageTimeoutMs controls how long tab creation waits for Firefox to create a page. If the context is unresponsive, Camofox replaces only that user's context and retries once. The default is 10 seconds.
Environment Variables
| Variable | Description | Default |
|---|---|---|
CAMOFOX_PORT |
Server port | 9377 |
PORT |
Server port (fallback, for platforms like Fly.io, Railway) | 9377 |
CAMOFOX_BIND_HOST |
Optional server bind host. Set to 127.0.0.1 for loopback-only access or 0.0.0.0 for IPv4 on all interfaces. When unset, Node uses its default all-interface binding. |
- |
CAMOFOX_API_KEY |
Enable cookie import endpoint (disabled if unset) | - |
CAMOFOX_ADMIN_KEY |
Required for POST /stop |
- |
CAMOFOX_ACCESS_KEY |
If set, all routes (except /health, cookie import, and /stop) require Authorization: Bearer <key>. Lets you safely expose the server beyond loopback. |
- |
CAMOFOX_EVALUATE_MAX_BODY_SIZE |
Max JSON request body size for POST /tabs/:tabId/evaluate; other JSON routes remain limited to 100kb. |
1mb |
CAMOFOX_LOCALE |
Locale for an explicitly configured direct-session identity. Must be set with CAMOFOX_TIMEZONE. |
- |
CAMOFOX_TIMEZONE |
IANA timezone for an explicitly configured direct-session identity. Must be set with CAMOFOX_LOCALE. |
- |
CAMOUFOX_EXECUTABLE |
External Camoufox executable to use instead of downloading/launching the bundled cache. Must point to a Camoufox bundle with sibling resources. | - |
CAMOUFOX_EXECUTABLE_PATH |
Compatibility alias for CAMOUFOX_EXECUTABLE |
- |
CAMOFOX_EXECUTABLE_PATH |
Compatibility alias for CAMOUFOX_EXECUTABLE |
- |
CAMOFOX_DISABLE_DEFAULT_ADDONS |
Set to 1/true to skip downloading and launching the default uBlock Origin (UBO) addon. Useful for deployments where the addons.mozilla.org download is unreliable or unwanted (a failed download otherwise leaves a broken addon cache that blocks startup). |
0 |
CAMOFOX_COOKIES_DIR |
Directory for cookie files | ~/.camofox/cookies |
CAMOFOX_UPLOADS_DIR |
Directory allowed for POST /tabs/:tabId/upload file attachments. Paths outside it, including symlink escapes, are rejected. |
~/.camofox/uploads |
CAMOFOX_PROFILE_DIR |
Directory for persisted session profiles | ~/.camofox/profiles |
CAMOFOX_TRACES_DIR |
Directory for session trace zips | ~/.camofox/traces |
CAMOFOX_TRACES_MAX_BYTES |
Max size per trace, removed on next startup if exceeded | 52428800 (50MB) |
CAMOFOX_TRACES_TTL_HOURS |
Traces older than this are swept on startup | 24 |
MAX_SESSIONS |
Max concurrent browser sessions | 50 |
MAX_TABS_PER_SESSION |
Max tabs per session | 10 |
SESSION_TIMEOUT_MS |
Session inactivity timeout (0 = never) | 600000 (10min) |
BROWSER_IDLE_TIMEOUT_MS |
Kill browser when idle (0 = never) | 300000 (5min) |
CAMOFOX_INTERACTIVE |
Interactive browser mode: desktop opens a real local Camoufox window; off keeps normal headless behavior |
off |
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_PROTOCOL |
Proxy protocol: http, https, socks4, or socks5. |
http |
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) |
CAMOFOX_CRASH_REPORT_ENABLED |
Enable anonymized crash/hang telemetry (false to disable) |
true |
CAMOFOX_CRASH_REPORT_URL |
Telemetry endpoint (self-hosted endpoint) | https://camofox-telemetry.askjo.workers.dev/report |
CAMOFOX_CRASH_REPORT_REPO |
GitHub repo for telemetry issues | jo-inc/camofox-browser |
CAMOFOX_CRASH_REPORT_RATE_LIMIT |
Max telemetry reports per hour | 10 |
ENABLE_VNC |
Enable VNC plugin for interactive browser access (1) |
- |
VNC_PASSWORD |
Password for VNC access (recommended in production) | - |
VNC_BIND |
noVNC listen address; set 0.0.0.0 to reach noVNC through a published container port |
127.0.0.1 |
VNC_RFB_BIND |
Native VNC listen address; non-loopback requires VNC_PASSWORD and does not change with VNC_BIND |
127.0.0.1 |
NOVNC_PORT |
noVNC web UI port | 6080 |
Interactive desktop browser
Camofox is headless by default. On a machine with a local graphical desktop, opt in to a visible Camoufox window:
CAMOFOX_INTERACTIVE=desktop npm startOr set the same preference in camofox.config.json and restart the server:
{
"interactive": { "mode": "desktop" }
}The environment variable overrides the file setting. Set CAMOFOX_INTERACTIVE=off to return to normal headless behavior. This mode is intended for a person using the same machine; it does not expose a remote browser-control service.
Architecture
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.
Security Model
This section documents the security boundaries of camofox-browser for users and automated security reviewers.
Code isolation
All process.env reads are centralized in lib/config.js. All child_process usage is in lib/launcher.js (server subprocess) and plugins/youtube/youtube.js (yt-dlp). The main server.js has route handlers but zero process.env reads and zero child_process imports. No single file combines environment/credential access with network sends.
No embedded secrets
Zero credentials, private keys, API tokens, or signing keys ship in this package. All secrets are provided at runtime via environment variables (CAMOFOX_API_KEY, CAMOFOX_ACCESS_KEY) or are Cloudflare Worker environment secrets (telemetry endpoint GitHub App key).
Cookie import is disabled by default
The cookie import endpoint (POST /sessions/:userId/cookies) is gated behind CAMOFOX_API_KEY. If this env var is not set, the server rejects all cookie import requests with HTTP 403. Cookie files are read from a sandboxed directory (~/.camofox/cookies/) with path traversal protection -- attempts to escape the directory are blocked. Max 500 cookies per request, 5MB file size limit.
Access control
CAMOFOX_ACCESS_KEY provides global bearer token authentication for all routes (except /health). When set, every request must include Authorization: Bearer <key>. Recommended for any deployment beyond localhost.
Binary download
The Camoufox browser engine (~300MB) is downloaded at npm install time by camoufox-js, an npm package maintained by the Camoufox project. It downloads from official GitHub releases with integrity verification handled by camoufox-js. No custom download URLs, no URL shorteners, no raw IP addresses.
Telemetry
Anonymized crash/hang telemetry is sent to a Cloudflare Worker endpoint. The endpoint source is in this repo and auditable. Verification: GET /source on the endpoint returns the deployed commit hash and sha256 so you can compare against the repo. The reporter (lib/reporter.js L28-290) applies paranoid anonymization: private domains are HMAC-hashed (not reversible), paths are stripped, tokens/IPs/emails are redacted. No page content, cookies, or user data is ever sent. Disable with CAMOFOX_CRASH_REPORT_ENABLED=false or point to your own endpoint with CAMOFOX_CRASH_REPORT_URL.
Session persistence
The persistence plugin saves cookies and localStorage to ~/.camofox/profiles/<hashed-userId>/ so authenticated sessions survive browser restarts. UserIds are hashed for directory names. Disable via camofox.config.json by removing persistence from the plugins array.
Network access
Outbound connections are made to: (1) URLs the agent navigates to (core functionality), (2) the telemetry endpoint (anonymized, opt-out available). Inbound: the REST API on port 9377, bound to all interfaces by default or to CAMOFOX_BIND_HOST when configured, optionally protected by CAMOFOX_ACCESS_KEY.
Subprocess usage
Two subprocesses may be spawned: (1) the Camoufox browser engine (core functionality, lib/launcher.js), (2) yt-dlp for YouTube transcript extraction (optional, plugins/youtube/youtube.js). Both are isolated in dedicated files separate from route handlers.
Testing
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
npm install @askjo/camofox-browserCredits
- Camoufox - Firefox-based browser with C++ anti-detection
- Donate to Camoufox's original creator daijro
- OpenClaw - Open-source AI agent framework
Crypto Scam Warning
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.
License
MIT
Overview (`mcp/README.md`)
camofox-browser MCP server
A standalone Model Context Protocol server that exposes camofox-browser to any MCP-compatible host — Claude Code, Cursor, etc. — without requiring OpenClaw.
It mirrors the existing OpenClaw plugin 1:1: same 11 tool names, identical JSON-Schema parameters, and the same REST routes. Whether an agent reaches camofox via OpenClaw or MCP, the behavior is identical.
The initial MCP server implementation was contributed by @epicsagas.
Architecture
The MCP server is a thin stdio client over the camofox REST server. Two pieces:
- REST server (
server.js) — launches Camoufox, exposes the HTTP API on:9377. Run once, it stays up. - MCP server (
mcp/server.mjs, exposed as thecamofox-browser-mcpbin) — translates MCP tool calls into REST calls. Claude Code spawns one per session.
Registering the MCP server does not require being inside the camofox-browser checkout. The examples below work from any directory.
mcp/ is also an independently installable package (@askjo/camofox-browser-mcp, its own package.json). It depends on nothing but @modelcontextprotocol/sdk — no camoufox-js, playwright-core, express, or the ~300MB browser binary download that the core server pulls in. Tool names, JSON-Schema parameters, REST routes, and response shaping are defined in mcp/lib/tool-contracts.mjs, which ships inside the standalone package. The OpenClaw plugin (plugin.ts) imports the same canonical module, so the two hosts cannot drift. See Option D below if you only want the MCP adapter (e.g. pointing CAMOFOX_BASE_URL at a REST server running elsewhere).
1. Start the REST server
Clone the repo and start the server (one-time binary download on first run):
git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser
npm install # downloads Camoufox (~300MB) on first run
npm start # → http://localhost:9377This stays running in the background. It does not need to be your cwd afterwards.
2. Install the `camofox-browser-mcp` bin
The MCP server is the same for every host — what differs is only the config file you paste into. First, make the camofox-browser-mcp bin available on your PATH. Pick one:
# Option A — npm link (if you have the source checkout; picks up local edits)
cd camofox-browser && npm link
# Option B — global install (no source checkout needed)
npm install @askjo/camofox-browser
# Option C — npx (no install; runs the published standalone adapter)
# Use this wherever a command is expected below.
npx -y @askjo/camofox-browser-mcp
# Option D — MCP adapter only, no core server deps (lightest footprint)
# Skips camoufox-js/playwright-core/express and the browser binary download —
# use this if a camofox-browser REST server is already running (locally or
# remotely) and you only need the stdio adapter.
git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser/mcp
npm install # installs only @modelcontextprotocol/sdk (~24MB, no postinstall)
npm link # camofox-browser-mcp now resolves from any directoryVerify the bin resolves from any directory:
which camofox-browser-mcp # → .../bin/camofox-browser-mcp3. Register with your host
All five hosts speak standard MCP, so they all run the same camofox-browser-mcp bin. Pick your host's config snippet below.
Claude Code
# CLI (user scope = available in every project)
claude mcp add camofox-browser -s user -- camofox-browser-mcpOr in ~/.claude.json (user) / .mcp.json (project, checked in):
{
"mcpServers": {
"camofox-browser": {
"command": "camofox-browser-mcp"
}
}
}Codex CLI
~/.codex/config.toml (user) or .codex/config.toml (project, trusted dirs only):
[mcp_servers.camofox-browser]
command = "camofox-browser-mcp"
env = { CAMOFOX_BASE_URL = "http://localhost:9377" }Antigravity / agy
Global ~/.gemini/config/mcp_config.json or workspace .agents/mcp_config.json:
{
"mcpServers": {
"camofox-browser": {
"command": "camofox-browser-mcp"
}
}
}Cursor
Global ~/.cursor/mcp.json or project .cursor/mcp.json (checked in):
{
"mcpServers": {
"camofox-browser": {
"command": "camofox-browser-mcp"
}
}
}(Or via UI: Settings → Cursor Settings → MCP → Add New MCP Server.)
opencode
opencode.json in the project root, or global ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"camofox-browser": {
"type": "local",
"command": ["camofox-browser-mcp"]
}
}
}Common: env vars and from-source fallback
If you need cookie import or a non-default REST URL, add env. Example for Claude Code:
claude mcp add camofox-browser -s user \
--env CAMOFOX_BASE_URL=http://localhost:9377 \
--env CAMOFOX_API_KEY=<key> \
-- camofox-browser-mcpFor the other hosts, add the same keys to the env / environment field of that host's snippet.
From-source fallback (only if you're inside the checkout and haven't linked the bin):
# Replace `camofox-browser-mcp` with `node /absolute/path/to/mcp/server.mjs`
claude mcp add camofox-browser -- node /Users/you/src/camofox-browser/mcp/server.mjs⚠️ Always prefer the
camofox-browser-mcpbin (options A/B/C/D above). Thenode ./mcp/server.mjsform is path-dependent — relative paths break outside the checkout.
4. Verify
| Host | How to verify |
|---|---|
| Claude Code | /mcp — camofox-browser shows connected |
| Codex CLI | codex then check MCP server list |
| agy | /mcp overlay in the agy CLI |
| Cursor | Settings → MCP — server shows green |
| opencode | opencode mcp list |
You should see 11 tools: camofox_create_tab, camofox_snapshot, camofox_click, camofox_type, camofox_navigate, camofox_scroll, camofox_screenshot, camofox_evaluate, camofox_list_tabs, camofox_close_tab, camofox_import_cookies.
Tools
| Tool | Purpose |
|---|---|
camofox_create_tab |
Open a URL → returns tabId |
camofox_snapshot |
Accessibility snapshot + element refs (e1, e2, ...) + screenshot |
camofox_navigate |
Go to a URL or use a search macro (@google_search, @reddit_search, ...) |
camofox_click |
Click by element ref (e1) or CSS selector |
camofox_type |
Type text into a ref/selector, optional pressEnter |
camofox_scroll |
Scroll by pixels (unreliable on lazy-load pages — prefer camofox_evaluate) |
camofox_screenshot |
Standalone screenshot |
camofox_evaluate |
Run JS in page context — extract data, call page APIs, scroll via window.scrollTo |
camofox_list_tabs |
List open tabs in this session |
camofox_close_tab |
Close a tab |
camofox_import_cookies |
Import a Netscape cookie file (needs CAMOFOX_API_KEY) |
Workflow
Every interaction follows the same shape — snapshot before you act:
create_tab({ url })→tabIdsnapshot({ tabId })→ element refs (e1,e2, ...)click/typeusing those refssnapshotagain to read the new stateclose_tabwhen done
Element refs are unambiguous and preferred over CSS selectors — a selector that matches multiple elements returns 422 strict mode violation, in which case re-snapshot and click by ref.
Environment variables
| Var | Default | Purpose |
|---|---|---|
CAMOFOX_BASE_URL |
http://localhost:9377 |
REST server URL |
CAMOFOX_USER_ID |
mcp-<random> |
Session isolation (one MCP server = one camofox session) |
CAMOFOX_SESSION_KEY |
default |
Tab partition within the user |
CAMOFOX_ACCESS_KEY |
(unset) | Global bearer token gating the REST server (superkey). If set, forwarded as Authorization: Bearer <key> on every tool call automatically — must match the REST server's own CAMOFOX_ACCESS_KEY. Without this, a globally-authenticated REST server rejects all tool calls except cookie import. |
CAMOFOX_API_KEY |
(unset) | Self-chosen secret gating cookie import. Optional on localhost; required on remote/production and must match the REST server's CAMOFOX_API_KEY. Only affects camofox_import_cookies. |
Troubleshooting
503 session_expired/tab create timed out— the REST server's browser session died (often after a prior failed call destabilized it). Restartnpm start.camofox_scrollreturns{ok:true}but the page doesn't move — expected on lazy-load / virtual-scroll pages; the server'smouse.wheelno-ops there. Usecamofox_evaluatewithwindow.scrollTo/scrollBy.422 strict mode violation ... resolved to N elementsonclick— CSS selector matched multiple elements. Re-snapshot and click by element ref.403 Forbiddenoncamofox_import_cookies— key mismatch between REST server and MCP server, or hitting a remote server withoutCAMOFOX_API_KEY.401/403on every tool call, not just cookie import — the REST server hasCAMOFOX_ACCESS_KEYset. Set the sameCAMOFOX_ACCESS_KEYin the MCP server's env (see table above) — it's forwarded automatically asAuthorization: Beareronce set.
Testing (developer)
Two layers, covering different things:
# 1. `test:mcp` installs the standalone adapter's locked dependencies, then
# runs the in-repository and packed-tarball handshake tests. Neither needs
# a REST server.
npm run test:mcp
# 2. Mock-HTTP contract tests — verifies the actual REST traffic each tool
# produces: routes, methods, request bodies, CAMOFOX_ACCESS_KEY/CAMOFOX_API_KEY
# auth headers, cookie parsing (Netscape file → `{ cookies }` body, never a
# path), screenshot decoding, and error handling. Mocked REST server — no
# live camofox-browser process needed. This is what makes the "OpenClaw and
# MCP send identical requests" claim verifiable, not just asserted.
NODE_OPTIONS='--experimental-vm-modules' npx jest tests/unit/mcp-contracts.test.jsThe packed-tarball check installs the generated @askjo/camofox-browser-mcp tarball in an empty directory before its handshake. It catches imports that reach outside the standalone package.
Overview (`plugins/persistence/README.md`)
persistence
Optional per-user browser storage state persistence for camofox-browser.
Saves and restores cookies and localStorage across session restarts, container deploys, and idle timeouts using Playwright's storageState API. IndexedDB persistence is available as an opt-in.
Configuration
In camofox.config.json:
{
"plugins": {
"persistence": {
"enabled": true,
"profileDir": "/data/profiles",
"indexedDB": true
}
}
}The profile directory can also be set via environment variable:
CAMOFOX_PROFILE_DIR=/data/profilesindexedDB(default:false) — set totrueto capture IndexedDB throughstorageState(). This can preserve logins stored there, including Firebase Auth and other SSO flows. It captures all serializable IndexedDB records—not only authentication data—and may make snapshots significantly larger and checkpoints slower.
How it works
- Session create: If a persisted
storageStateexists for theuserId, it's restored into the new Playwright context. - First run: If no persisted state exists, bootstrap cookies from
CAMOFOX_COOKIES_DIR/cookies.txtare imported (if present). - Cookie import / session close / shutdown: Storage state is checkpointed to disk via atomic tmp-write + rename.
- Session reset:
DELETE /sessions/:userId/storage_statecloses the live context without checkpointing it, waits for in-flight writes, and deletes persisted state so the next session starts fresh. - User isolation: Each
userIdmaps to a deterministic SHA256-hashed subdirectory underprofileDir, so arbitrary userIds are path-safe.
Docker
When running with Docker, mount the profile directory as a volume:
docker run -d \
-p 9377:9377 \
-v /host/profiles:/data/profiles \
camofox-browserCredits
Based on PR #62 by company8.
Overview (`plugins/vnc/README.md`)
VNC Plugin
Originally contributed by @leoneparise in PR #65. Reworked as a plugin for the camofox extension system.
Interactive browser access via VNC. Log into sites visually, solve CAPTCHAs, approve OAuth prompts — then export the authenticated storage state for reuse by your agent.
How it works
Camoufox (Xvfb :99, 1920x1080)
↑
x11vnc (attaches to :99, port 5900)
↑
noVNC / websockify (port 6080)
↑
Your browser → http://localhost:6080/vnc.htmlThe plugin registers the virtualDisplay capability for its own plugins.vnc settings, selecting a human-usable resolution instead of the default 1x1 display. A second enabled display plugin cannot silently replace that provider. The watcher handles browser restarts automatically — when Camoufox relaunches on a new display, x11vnc reattaches.
Quick start
Docker
docker run -p 9377:9377 -p 127.0.0.1:6080:6080 \
-e ENABLE_VNC=1 -e VNC_BIND=0.0.0.0 \
camofox-browser
# Open http://localhost:6080/vnc.html in your browserConfig file
{
"plugins": {
"vnc": {
"enabled": true,
"resolution": "1920x1080",
"password": "optional-secret",
"viewOnly": false,
"novncPort": 6080
}
}
}Workflow: interactive login → agent reuse
Start with VNC enabled:
bashdocker run -p 9377:9377 -p 127.0.0.1:6080:6080 -e ENABLE_VNC=1 -e VNC_BIND=0.0.0.0 camofox-browserCreate a session and navigate to the login page:
bashcurl -X POST http://localhost:9377/tabs \ -H 'Content-Type: application/json' \ -d '{"userId": "my-agent", "sessionKey": "default", "url": "https://accounts.google.com"}'Log in visually via http://localhost:6080/vnc.html — complete MFA, solve CAPTCHAs, etc.
Export the authenticated state:
bashcurl http://localhost:9377/sessions/my-agent/storage_state \ -H 'Authorization: Bearer YOUR_CAMOFOX_API_KEY' \ -o storage_state.jsonReuse on future runs — pair with the persistence plugin to automatically restore state on session creation:
json{ "plugins": { "vnc": { "enabled": true }, "persistence": { "enabled": true, "profileDir": "/data/profiles" } } }With both plugins active, the persistence plugin automatically checkpoints storage state on session close and restores it on creation. The VNC plugin's export endpoint also triggers a persistence checkpoint via the
session:storage:exportevent.
API
GET /vnc/status
Report whether the VNC watcher and its x11vnc backend are running. This endpoint does not require authentication and exposes no password or session data.
{
"enabled": true,
"running": true,
"watcherRunning": true,
"display": ":0",
"vncPort": 5900,
"novncPort": 6080,
"path": "/vnc.html"
}running becomes false while no owned X display is attached or while x11vnc is being restarted. watcherRunning distinguishes that state from the watcher process itself having exited.
GET /sessions/:userId/storage_state
Export the full Playwright storage state (cookies + localStorage origins) for a user's active browser context.
Auth: Same as cookie import — requires CAMOFOX_API_KEY Bearer token, or loopback access in non-production.
Response:
{
"cookies": [
{
"name": "session_id",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"expires": 1700000000,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "https://example.com",
"localStorage": [
{ "name": "theme", "value": "dark" }
]
}
]
}Errors:
404— No active session for the given userId403— Missing or invalid API key500— Context is dead or storageState export failed
Configuration
| Source | Variable | Description | Default |
|---|---|---|---|
| env | ENABLE_VNC |
Enable the plugin (1) |
off |
| env | VNC_PASSWORD |
x11vnc password | none (open) |
| env | VNC_BIND |
noVNC listener address; use 0.0.0.0 in a container with host-local port publishing |
127.0.0.1 |
| env | VNC_RFB_BIND |
Native VNC listener address; non-loopback requires VNC_PASSWORD |
127.0.0.1 |
| env | VNC_RESOLUTION |
Xvfb screen resolution | 1920x1080 |
| env | VIEW_ONLY |
Disable mouse/keyboard input (1) |
off |
| env | VNC_PORT |
x11vnc listen port | 5900 |
| env | NOVNC_PORT |
noVNC web UI port | 6080 |
| config | plugins.vnc.enabled |
Enable the plugin | false |
| config | plugins.vnc.password |
x11vnc password | none |
| config | plugins.vnc.resolution |
Xvfb screen resolution | 1920x1080 |
| config | plugins.vnc.viewOnly |
View-only mode | false |
| config | plugins.vnc.vncPort |
x11vnc listen port | 5900 |
| config | plugins.vnc.novncPort |
noVNC web UI port | 6080 |
Environment variables override config file values.
Security
⚠️ VNC is unencrypted by default. When running in production:
- Set
VNC_PASSWORD— without it, anyone who can reach port 6080 has full browser control - Bind 6080 to localhost and access via SSH tunnel:
ssh -L 6080:localhost:6080 your-server - Or use a firewall to restrict access to port 6080
- In Docker: set
VNC_BIND=0.0.0.0for container forwarding and-p 127.0.0.1:6080:6080to bind the host port only to localhost. To expose native VNC separately, setVNC_RFB_BIND=0.0.0.0andVNC_PASSWORD, then publish port 5900 only to trusted hosts.
System dependencies
The plugin declares its apt dependencies in apt.txt — these are installed automatically during docker build via scripts/install-plugin-deps.sh:
x11vnc— attaches to Xvfb displaynovnc+python3-websockify— web-based VNC clientnet-tools+procps— display detection utilities
Events
| Event | Payload | Description |
|---|---|---|
vnc:watcher:started |
{ pid } |
Watcher process spawned |
vnc:watcher:stopped |
{ code, signal } |
Watcher exited |
vnc:storage:exported |
{ userId, cookies, origins } |
Storage state exported via API |
session:storage:export |
{ userId, storageState } |
Awaited after export; the persistence plugin writes the same snapshot to disk without serializing the browser again |
Overview (`tools/windows-utm/README.md`)
Windows ARM64 UTM E2E runner (macOS)
This directory creates a new, isolated Windows 11 ARM64 virtual machine (VM) in UTM for reproducing Camofox Windows failures locally. It is developer tooling, not part of the Camofox runtime or CI job.
The GitHub Actions Windows job remains the normal test path. Use this only when a hosted Windows failure needs interactive or SSH-based investigation.
What is checked in
build-installer-autoboot-iso.shcreates a derived, verified copy of Microsoft's ARM64 installer with a UEFI fallback so UTM can boot it unattended.build-fat-bootstrap.pycreates a UEFI-readable FAT32 bootstrap image from that installer.build-provisioning-iso.shcreates the one-time Windows Setup answer ISO and embeds one supplied public SSH key.create-vm.shassembles those inputs into a new UTM bundle with an isolated sparse NVMe disk and loopback-only SSH forwarding.
Do not commit the Windows installer, generated ISOs or images, UTM bundles, VM disks, UEFI/TPM state, debug logs, or SSH keys.
Requirements
macOS on Apple silicon
UTM installed in
/Applications/UTM.appqemu-imgonPATH(for example,brew install qemu)Python 3
A Microsoft Windows 11 ARM64 ISO whose SHA-256 is:
text638AA2C88E94385B00F4F178D071E3DF0B7D9E335577A83BD533B7F2EB65ADF0The scripts refuse another image rather than silently using an unreviewed Windows build.
Create the VM
Run these commands from the repository root. Choose an artifact directory outside the repository; generated files are large.
mkdir -p "$HOME/Downloads/camofox-windows-e2e"
ssh-keygen -t ed25519 -f "$HOME/.ssh/camofox-windows-e2e" -C camofox-windows-e2e
# Replace this with the verified Microsoft ARM64 installer you downloaded.
WINDOWS_ISO="$HOME/Downloads/Win11_25H2_English_Arm64_v2.iso"
ARTIFACTS="$HOME/Downloads/camofox-windows-e2e"
# The derived installer retains Microsoft's files and adds only startup.nsh.
tools/windows-utm/build-installer-autoboot-iso.sh \
--source "$WINDOWS_ISO" \
--output "$ARTIFACTS/windows-installer.iso"
# This is for UTM firmware that cannot read the Windows UDF installer directly.
tools/windows-utm/build-fat-bootstrap.py \
--source "$ARTIFACTS/windows-installer.iso" \
--output "$ARTIFACTS/windows-fat-bootstrap.img"
# New-VM-only answer ISO. It embeds the public key, never the private key.
tools/windows-utm/build-provisioning-iso.sh \
--public-key "$HOME/.ssh/camofox-windows-e2e.pub" \
--output "$ARTIFACTS/camofox-windows-provisioning.iso"
# Creates a new VM; it refuses to overwrite an existing bundle.
tools/windows-utm/create-vm.sh \
--installer "$ARTIFACTS/windows-installer.iso" \
--provisioning "$ARTIFACTS/camofox-windows-provisioning.iso" \
--bootstrap "$ARTIFACTS/windows-fat-bootstrap.img"The final command opens UTM. Let Windows Setup finish. It creates the unprivileged camofox-e2e account, installs OpenSSH Server, and forwards guest port 22 only to 127.0.0.1:22222 on the Mac.
Once Windows is up, verify SSH:
ssh -i "$HOME/.ssh/camofox-windows-e2e" -p 22222 [email protected]Then use that SSH session to run the bounded Windows test or inspect the Windows setup log at C:\ProgramData\CamofoxE2E\provision.log.
Safety
build-provisioning-iso.sh creates an Autounattend.xml that wipes and partitions disk 0. Use its result only as installation media for a freshly created VM. Never attach it to an existing Windows machine or VM.
The VM creation script never overwrites an existing UTM bundle, and its SSH forward is loopback-only. Delete the entire generated UTM bundle and the artifact directory when the investigation is complete.
Openapi.Json (`docs/openapi.json`)
{
"openapi": "3.0.3",
"info": {
"title": "camofox-browser",
"version": "1.6.0",
"description": "Anti-detection browser automation server for AI agents. Accessibility snapshots, element refs, session isolation, cookie import, proxy rotation, and structured logs.",
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
},
"contact": {
"name": "Jo Inc",
"url": "https://askjo.ai",
"email": "[email protected]"
}
},
"servers": [
{
"url": "http://localhost:9377",
"description": "Local development"
}
],
"tags": [
{
"name": "System",
"description": "Server health, metrics, and status."
},
{
"name": "Tabs",
"description": "Create, list, inspect, and destroy browser tabs."
},
{
"name": "Navigation",
"description": "Navigate tabs to URLs or via search macros."
},
{
"name": "Interaction",
"description": "Click, type, scroll, press keys, evaluate JS."
},
{
"name": "Content",
"description": "Accessibility snapshots, screenshots, links, images, downloads."
},
{
"name": "Sessions",
"description": "Per-user session state: cookies, teardown."
},
{
"name": "Browser",
"description": "Global browser lifecycle (start/stop)."
},
{
"name": "Legacy",
"description": "OpenClaw-compatible endpoints (deprecated)."
}
],
"components": {
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"description": "Bearer token matching CAMOFOX_API_KEY."
}
},
"schemas": {
"Error": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string"
}
}
}
}
},
"paths": {
"/sessions/{userId}/cookies": {
"post": {
"tags": [
"Sessions"
],
"summary": "Import cookies into a user session",
"description": "Import cookies for authenticated browsing. Requires BearerAuth in production.",
"security": [
{
"BearerAuth": []
}
],
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Session owner identifier."
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"cookies"
],
"properties": {
"cookies": {
"type": "array",
"maxItems": 500,
"items": {
"type": "object",
"required": [
"name",
"value",
"domain"
],
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
},
"domain": {
"type": "string"
},
"path": {
"type": "string"
},
"expires": {
"type": "number"
},
"httpOnly": {
"type": "boolean"
},
"secure": {
"type": "boolean"
},
"sameSite": {
"type": "string",
"enum": [
"Strict",
"Lax",
"None"
]
}
}
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "Cookies imported.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"userId": {
"type": "string"
},
"count": {
"type": "integer"
}
}
}
}
}
},
"400": {
"description": "Invalid cookie data.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/health": {
"get": {
"tags": [
"System"
],
"summary": "Health check",
"description": "Detailed health with tab/session counts and failure tracking.",
"responses": {
"200": {
"description": "Healthy.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"engine": {
"type": "string"
},
"browserConnected": {
"type": "boolean"
},
"browserRunning": {
"type": "boolean"
},
"activeTabs": {
"type": "integer"
},
"activeSessions": {
"type": "integer"
},
"consecutiveFailures": {
"type": "integer"
}
}
}
}
}
},
"503": {
"description": "Unhealthy or recovering.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"recovering": {
"type": "boolean"
}
}
}
}
}
}
}
}
},
"/metrics": {
"get": {
"tags": [
"System"
],
"summary": "Prometheus metrics",
"description": "Returns Prometheus text exposition format. Requires PROMETHEUS_ENABLED=1.",
"responses": {
"200": {
"description": "Prometheus metrics.",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"404": {
"description": "Metrics disabled.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs": {
"post": {
"tags": [
"Tabs"
],
"summary": "Create a new tab",
"description": "Creates a tab in the given session. Optionally navigates to an initial URL.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId",
"sessionKey"
],
"properties": {
"userId": {
"type": "string",
"description": "Session owner."
},
"sessionKey": {
"type": "string",
"description": "Tab group identifier."
},
"listItemId": {
"type": "string",
"description": "Legacy alias for sessionKey."
},
"url": {
"type": "string",
"description": "Optional initial URL."
},
"trace": {
"type": "boolean",
"description": "Enable Playwright tracing for this session (screenshots, DOM snapshots, network). Must be set on first tab creation; cannot be added to an existing session."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Tab created.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tabId": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
}
},
"400": {
"description": "Missing required fields.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Cannot enable tracing on an existing session.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": {
"description": "Tab limit reached.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"get": {
"tags": [
"Tabs"
],
"summary": "List open tabs",
"description": "Returns all tabs for a given userId.",
"parameters": [
{
"name": "userId",
"in": "query",
"schema": {
"type": "string"
},
"description": "Filter by session owner."
}
],
"responses": {
"200": {
"description": "Tab list.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"running": {
"type": "boolean"
},
"tabs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tabId": {
"type": "string"
},
"targetId": {
"type": "string"
},
"url": {
"type": "string"
},
"title": {
"type": "string"
},
"listItemId": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
},
"/tabs/{tabId}/navigate": {
"post": {
"tags": [
"Navigation"
],
"summary": "Navigate a tab to a URL or macro",
"description": "Navigate to a URL or expand a search macro. Auto-creates tab if not found.",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
},
"url": {
"type": "string"
},
"macro": {
"type": "string",
"description": "Search macro (e.g. @google_search)."
},
"query": {
"type": "string",
"description": "Search query for macro."
},
"sessionKey": {
"type": "string"
},
"listItemId": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Navigation result with snapshot.",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/snapshot": {
"get": {
"tags": [
"Content"
],
"summary": "Accessibility snapshot",
"description": "Returns accessibility tree with element refs. Supports pagination via offset.",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "format",
"in": "query",
"schema": {
"type": "string",
"enum": [
"text",
"json"
],
"default": "text"
}
},
{
"name": "offset",
"in": "query",
"schema": {
"type": "integer"
},
"description": "Character offset for paginated retrieval."
},
{
"name": "includeScreenshot",
"in": "query",
"schema": {
"type": "string",
"enum": [
"true",
"false"
]
}
}
],
"responses": {
"200": {
"description": "Snapshot.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"snapshot": {
"type": "string"
},
"refsCount": {
"type": "integer"
},
"truncated": {
"type": "boolean"
},
"totalChars": {
"type": "integer"
},
"hasMore": {
"type": "boolean"
},
"nextOffset": {
"type": "integer"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/wait": {
"post": {
"tags": [
"Interaction"
],
"summary": "Wait for a selector or timeout",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
},
"selector": {
"type": "string"
},
"timeout": {
"type": "integer",
"description": "Max wait in ms."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Wait completed.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/click": {
"post": {
"tags": [
"Interaction"
],
"summary": "Click an element",
"description": "Click by element ref, CSS selector, or coordinates.",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
},
"ref": {
"type": "string",
"description": "Element ref ID (e.g. "e3")."
},
"selector": {
"type": "string",
"description": "CSS selector fallback."
},
"doubleClick": {
"type": "boolean"
},
"coordinates": {
"type": "object",
"properties": {
"x": {
"type": "number"
},
"y": {
"type": "number"
}
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "Click result with optional post-action snapshot.",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/type": {
"post": {
"tags": [
"Interaction"
],
"summary": "Type text into an element",
"description": "Types text into a focused element or a specific ref/selector.",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId",
"text"
],
"properties": {
"userId": {
"type": "string"
},
"ref": {
"type": "string"
},
"selector": {
"type": "string"
},
"text": {
"type": "string"
},
"clear": {
"type": "boolean",
"description": "Clear field before typing."
},
"submit": {
"type": "boolean",
"description": "Press Enter after typing."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Type result.",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/press": {
"post": {
"tags": [
"Interaction"
],
"summary": "Press a keyboard key",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId",
"key"
],
"properties": {
"userId": {
"type": "string"
},
"key": {
"type": "string",
"description": "Key name (e.g. "Enter", "Escape", "Tab")."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Key pressed.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/scroll": {
"post": {
"tags": [
"Interaction"
],
"summary": "Scroll the page",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
},
"direction": {
"type": "string",
"description": ""up" or "down" (default "down")."
},
"amount": {
"type": "integer",
"description": "Pixels to scroll."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Scroll result.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/back": {
"post": {
"tags": [
"Navigation"
],
"summary": "Go back",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Navigated back.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"url": {
"type": "string"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/forward": {
"post": {
"tags": [
"Navigation"
],
"summary": "Go forward",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Navigated forward.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"url": {
"type": "string"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/refresh": {
"post": {
"tags": [
"Navigation"
],
"summary": "Refresh page",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Page refreshed.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"url": {
"type": "string"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/links": {
"get": {
"tags": [
"Content"
],
"summary": "Extract page links",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Links extracted.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"href": {
"type": "string"
},
"ref": {
"type": "string"
}
}
}
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/downloads": {
"get": {
"tags": [
"Content"
],
"summary": "List tab downloads",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Downloads list.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"downloads": {
"type": "array",
"items": {
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"url": {
"type": "string"
},
"state": {
"type": "string"
}
}
}
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/images": {
"get": {
"tags": [
"Content"
],
"summary": "Extract page images",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Images extracted.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"src": {
"type": "string"
},
"alt": {
"type": "string"
},
"width": {
"type": "integer"
},
"height": {
"type": "integer"
}
}
}
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/screenshot": {
"get": {
"tags": [
"Content"
],
"summary": "Take a screenshot",
"description": "Returns a base64-encoded PNG screenshot.",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Screenshot.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"screenshot": {
"type": "object",
"properties": {
"data": {
"type": "string"
},
"mimeType": {
"type": "string"
}
}
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/stats": {
"get": {
"tags": [
"Tabs"
],
"summary": "Tab statistics",
"description": "Returns tab metadata including URL, tool call count, visited URLs, download/failure counts.",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Tab stats.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tabId": {
"type": "string"
},
"url": {
"type": "string"
},
"toolCalls": {
"type": "integer"
},
"visitedUrls": {
"type": "array",
"items": {
"type": "string"
}
},
"downloadCount": {
"type": "integer"
},
"consecutiveFailures": {
"type": "integer"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/evaluate": {
"post": {
"tags": [
"Interaction"
],
"summary": "Evaluate JavaScript in tab",
"description": "Runs arbitrary JS in the page context and returns the result.",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId",
"expression"
],
"properties": {
"userId": {
"type": "string"
},
"expression": {
"type": "string",
"description": "JavaScript expression to evaluate."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Evaluation result.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"result": {}
}
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}/extract": {
"post": {
"tags": [
"Content"
],
"summary": "Structured data extraction via JSON Schema",
"description": "Extracts structured data from the current page using a JSON Schema whose properties\ncarry x-ref hints pointing at snapshot element refs (e.g. e1, e2). \nCall GET /tabs/{tabId}/snapshot first to populate the ref table.\n",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId",
"schema"
],
"properties": {
"userId": {
"type": "string"
},
"schema": {
"type": "object",
"description": "JSON Schema with type: \"object\" and a properties map. \nEach property may include x-ref (a snapshot element ref) and an optional\ntype (string, number, integer, boolean).\n",
"required": [
"type",
"properties"
],
"properties": {
"type": {
"type": "string",
"enum": [
"object"
]
},
"properties": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"string",
"number",
"integer",
"boolean",
"object",
"null"
]
},
"x-ref": {
"type": "string",
"description": "Snapshot element ref (e.g. e1)."
}
}
}
},
"required": {
"type": "array",
"items": {
"type": "string"
},
"description": "Property names that must resolve to a non-null value."
}
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "Extraction succeeded.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"data": {
"type": "object",
"description": "Extracted key-value pairs matching the input schema."
}
}
}
}
}
},
"400": {
"description": "Missing userId, missing schema, or invalid schema.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "No refs available -- call snapshot first.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"snapshot": {
"type": "string",
"nullable": true
}
}
}
}
}
},
"422": {
"description": "Extraction failed (e.g. required ref not found).",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"error": {
"type": "string"
},
"snapshot": {
"type": "string",
"nullable": true
}
}
}
}
}
},
"500": {
"description": "Internal server error.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/{tabId}": {
"delete": {
"tags": [
"Tabs"
],
"summary": "Close a tab",
"parameters": [
{
"name": "tabId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Tab closed.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/tabs/group/{listItemId}": {
"delete": {
"tags": [
"Tabs"
],
"summary": "Close all tabs in a group",
"parameters": [
{
"name": "listItemId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Group closed.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"closed": {
"type": "integer"
}
}
}
}
}
},
"404": {
"description": "Session not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/sessions/{userId}/traces": {
"get": {
"tags": [
"Sessions"
],
"summary": "List trace files",
"description": "Returns all Playwright trace zip files for the given user session, sorted newest first.",
"security": [
{
"BearerAuth": []
}
],
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Session owner identifier."
}
],
"responses": {
"200": {
"description": "Trace list.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"traces": {
"type": "array",
"items": {
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"sizeBytes": {
"type": "integer"
},
"createdAt": {
"type": "number"
},
"modifiedAt": {
"type": "number"
}
}
}
}
}
}
}
}
},
"403": {
"description": "Forbidden.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Server error.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/sessions/{userId}/traces/{filename}": {
"get": {
"tags": [
"Sessions"
],
"summary": "Download a trace file",
"description": "Streams a Playwright trace zip for viewing in trace.playwright.dev.",
"security": [
{
"BearerAuth": []
}
],
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Session owner identifier."
},
{
"name": "filename",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Trace zip filename."
}
],
"responses": {
"200": {
"description": "Trace zip stream.",
"content": {
"application/zip": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"400": {
"description": "Invalid filename.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Trace not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Server error.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"delete": {
"tags": [
"Sessions"
],
"summary": "Delete a trace file",
"description": "Removes a specific Playwright trace zip from the server.",
"security": [
{
"BearerAuth": []
}
],
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Session owner identifier."
},
{
"name": "filename",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Trace zip filename."
}
],
"responses": {
"200": {
"description": "Trace deleted.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
}
}
}
}
},
"400": {
"description": "Invalid filename.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Forbidden.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Trace not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Server error.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/sessions/{userId}": {
"delete": {
"tags": [
"Sessions"
],
"summary": "Destroy a user session",
"description": "Closes all tabs and cleans up state for the given userId.",
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Session destroyed.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"closed": {
"type": "integer"
}
}
}
}
}
},
"404": {
"description": "Session not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/": {
"get": {
"tags": [
"System"
],
"summary": "Server status",
"description": "Returns basic server liveness and browser state.",
"responses": {
"200": {
"description": "Server status.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
},
"running": {
"type": "boolean"
},
"engine": {
"type": "string"
},
"browserConnected": {
"type": "boolean"
},
"browserRunning": {
"type": "boolean"
}
}
}
}
}
}
}
}
},
"/tabs/open": {
"post": {
"tags": [
"Legacy"
],
"summary": "Open tab (OpenClaw format)",
"deprecated": true,
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId",
"url"
],
"properties": {
"userId": {
"type": "string"
},
"url": {
"type": "string"
},
"listItemId": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Tab opened.",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/start": {
"post": {
"tags": [
"Browser"
],
"summary": "Start browser",
"description": "Ensures the browser process is running. Idempotent.",
"responses": {
"200": {
"description": "Browser started.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"profile": {
"type": "string"
}
}
}
}
}
},
"500": {
"description": "Launch failed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/stop": {
"post": {
"tags": [
"Browser"
],
"summary": "Stop browser",
"description": "Stops the browser and closes all sessions. Requires x-admin-key header.",
"security": [
{
"BearerAuth": []
}
],
"responses": {
"200": {
"description": "Browser stopped.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"stopped": {
"type": "boolean"
},
"profile": {
"type": "string"
}
}
}
}
}
},
"403": {
"description": "Forbidden.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/navigate": {
"post": {
"tags": [
"Legacy"
],
"summary": "Navigate (OpenClaw format)",
"description": "Navigate with targetId in body instead of path param.",
"deprecated": true,
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId",
"url"
],
"properties": {
"userId": {
"type": "string"
},
"targetId": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Navigation result.",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/snapshot": {
"get": {
"tags": [
"Legacy"
],
"summary": "Snapshot (OpenClaw format)",
"description": "Snapshot with targetId/userId as query params.",
"deprecated": true,
"parameters": [
{
"name": "targetId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "format",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "offset",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "includeScreenshot",
"in": "query",
"schema": {
"type": "string",
"enum": [
"true",
"false"
]
}
}
],
"responses": {
"200": {
"description": "Snapshot.",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/act": {
"post": {
"tags": [
"Legacy"
],
"summary": "Combined action (OpenClaw format)",
"description": "Routes to click/type/scroll/press/etc based on "kind" parameter.",
"deprecated": true,
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"userId",
"kind"
],
"properties": {
"userId": {
"type": "string"
},
"kind": {
"type": "string",
"description": "Action kind: click, type, scroll, press, key, select_option, drag, hover, screenshot, wait, back, forward."
},
"targetId": {
"type": "string"
},
"ref": {
"type": "string"
},
"selector": {
"type": "string"
},
"text": {
"type": "string"
},
"key": {
"type": "string"
},
"direction": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Action result.",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Tab not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
}
}
--- METRICS ---
- Files Extracted: 6
- Estimated Token Budget: ~32933 tokens
- Canonical Reference: https://codewiki.google/github.com/jo-inc/camofox-browser