camofox-browser
Headless browser automation server wrapping Camoufox (Firefox fork with C++-level fingerprint spoofing) behind a REST API designed for AI agents. Default port 9377. Source: https://github.com/jo-inc/camofox-browser
What It Is
A Node.js/Express server (server.js) that exposes a Camoufox browser engine as HTTP endpoints. Agents create tabs, fetch token-efficient accessibility snapshots with stable element refs (e1, e2, …), and interact by ref rather than by CSS selector or raw coordinates. Cookies, proxies, search macros, YouTube transcript extraction, and session persistence are built in. Plugins extend the server with new routes, lifecycle hooks, and Prometheus metrics.
Architecture
Browser Instance (Camoufox)
└── User Session (BrowserContext) — isolated cookies/storage per userId
├── Tab Group (sessionKey) — groups tabs by conversation/task
│ ├── Tab (google.com)
│ └── Tab (github.com)
└── Tab Group
└── Tab
Key behaviors:
- Lazy browser launch. Idle browser shuts down after
BROWSER_IDLE_TIMEOUT_MS(default 5min), relaunches on next request. Idle memory ≈ 40 MB. - Sessions auto-expire after
SESSION_TIMEOUT_MS(default 30 min). - When a session hits
MAX_TABS_PER_SESSION, the oldest/least-used tab is recycled instead of erroring. - All logs are one JSON object per line (
{ts, level, msg, reqId, …})./healthis excluded from request logging.
Install & Run
Standalone:
git clone https://github.com/jo-inc/camofox-browser
cd camofox-browser
npm install
npm start # downloads Camoufox on first run (~300MB)
# → http://localhost:9377
As an OpenClaw plugin:
openclaw plugins install @askjo/camofox-browser
Docker (uses provided Makefile, which pre-downloads Camoufox + yt-dlp outside the build via bind mounts):
make up # auto-detects ARCH (aarch64 on M1/M2, x86_64 on Intel)
make down
make reset # clean rebuild
make fetch # only download binaries
make up ARCH=x86_64 VERSION=135.0.1 RELEASE=beta.24
Never run docker build directly — the Dockerfile relies on bind mounts populated by make fetch.
Fly.io / Railway: railway.toml is included. For Fly.io, use a Dockerfile that downloads binaries at build time (the bind-mount Dockerfile won't work in remote CI). Reference: jo-browser.
Full deployment details including the with-plugins Docker stage, scripts/install-plugin-deps.sh, and railway.toml: references/deployment.md.
Core Agent Workflow
POST /tabs → { tabId }
GET /tabs/:tabId/snapshot → accessibility tree with element refs e1, e2, …
POST /tabs/:tabId/click { ref } → interact
POST /tabs/:tabId/type { ref } → interact
POST /tabs/:tabId/navigate → URL or search macro
DELETE /tabs/:tabId → cleanup
Element refs are stable within a snapshot but reset on navigation — always fetch a fresh snapshot after navigating. Snapshots are ~90% smaller than raw HTML because they're accessibility-tree based.
Full workflow patterns (pagination via offset, screenshots with includeScreenshot=true, CSS-selector fallback, pressEnter, etc.): references/agent-usage.md.
Authentication
Two keys, both optional, both read from the environment (never from config files):
| Variable | Enables | Behavior if unset |
|---|---|---|
CAMOFOX_API_KEY |
Cookie import endpoint (POST /sessions/:userId/cookies) and any route using ctx.auth() |
Server returns 403 on those routes |
CAMOFOX_ADMIN_KEY |
POST /stop |
Endpoint rejected |
Requests authenticate via Authorization: Bearer <key>. Loopback (127.0.0.1) requests bypass the bearer check. Generate a key with openssl rand -hex 32.
REST API Surface
High-level groups — full schemas and query parameters: references/api-reference.md.
| Group | Endpoints |
|---|---|
| Tab lifecycle | POST /tabs, GET /tabs, GET /tabs/:id/stats, DELETE /tabs/:id, DELETE /tabs/group/:groupId, DELETE /sessions/:userId |
| Page interaction | GET /tabs/:id/snapshot, POST /tabs/:id/click, /type, /press, /scroll, /navigate, /wait, GET /tabs/:id/links, /images, /downloads, /screenshot, POST /tabs/:id/back, /forward, /refresh |
| YouTube | POST /youtube/transcript (yt-dlp fast path, browser fallback) |
| Sessions | POST /sessions/:userId/cookies, GET /sessions/:userId/storage_state |
| Server | GET /health, POST /start, POST /stop |
Search Macros
Navigate via {"macro": "@<name>", "query": "…"} instead of constructing search URLs:
@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). @reddit_subreddit with query "programming" fetches /r/programming.json.
Sessions, Cookies, Proxies, VNC
- Per-user session isolation via
userId; tab grouping viasessionKey(legacy alias:listItemId). - Cookie import: Netscape-format files under
~/.camofox/cookies/(override withCAMOFOX_COOKIES_DIR). Max 500 cookies per request, 5 MB per file, fields sanitized to a Playwright allowlist, path-traversal blocked. - Session persistence:
persistenceplugin writes each user'sstorage_state.jsonunder~/.camofox/profiles/<hashed-userId>/. Disable via"persistence": { "enabled": false }incamofox.config.jsonor override path withCAMOFOX_PROFILE_DIR. - Proxy modes: simple (single endpoint via
PROXY_HOST/PROXY_PORT) orPROXY_STRATEGY=backconnect(rotating sticky sessions for Decodo/Bright Data/Oxylabs). Camoufox's GeoIP auto-sets locale, timezone, and geolocation from the proxy's exit IP. - VNC interactive login: set
ENABLE_VNC=1+VNC_PASSWORD. noVNC web UI onNOVNC_PORT(default6080). Export resulting auth state viaGET /sessions/:userId/storage_state.
Details (cookie format, backconnect session rotation, storage state reuse, VNC workflow): references/sessions-cookies-proxy.md.
Environment Variables
Full table with defaults: references/deployment.md. Most commonly set:
| Variable | Default |
|---|---|
CAMOFOX_PORT / PORT |
9377 |
CAMOFOX_API_KEY |
unset (cookie import disabled) |
MAX_SESSIONS |
50 |
MAX_TABS_PER_SESSION |
10 |
SESSION_TIMEOUT_MS |
1800000 (30 min) |
BROWSER_IDLE_TIMEOUT_MS |
300000 (5 min) |
HANDLER_TIMEOUT_MS |
30000 |
MAX_CONCURRENT_PER_USER |
3 |
MAX_OLD_SPACE_SIZE |
128 (MB, Node V8 heap) |
PROXY_STRATEGY |
unset (simple mode) |
PROMETHEUS_ENABLED |
unset (metrics off) |
Plugin System
Plugins live under plugins/<name>/index.js and export register(app, ctx). Loaded per camofox.config.json ({"plugins": ["youtube", …]}). If the file or plugins key is missing, all plugins in plugins/ load (backward-compatible).
Install third-party plugins via CLI:
npm run plugin install https://github.com/user/camofox-screenshot-plugin
npm run plugin install git:github.com/user/my-plugin
npm run plugin install ./path/to/my-plugin
npm run plugin list
npm run plugin remove my-plugin
Default plugins shipped:
| Plugin | Enabled | Purpose |
|---|---|---|
youtube |
yes | /youtube/transcript via yt-dlp (fast) or browser fallback |
persistence |
yes | Per-user storage state on disk |
vnc |
no (requires ENABLE_VNC=1) |
Interactive browser login via noVNC |
OpenClaw Scanner Isolation (critical constraint)
The OpenClaw skill-scanner flags any single .js file that contains BOTH halves of a rule pair. Violations break publication. Rules:
env-harvesting(CRITICAL):/process\.env/+/\bfetch\b|\bpost\b|http\.request/iin same file. Regex is case-insensitive — string literals like'POST'and comments mentioningprocess.envtrigger it.dangerous-exec(CRITICAL):child_processimport +exec/spawncall in same file.potential-exfiltration(WARN):readFile+fetch/post/http.requestin same file.
Enforced layout:
process.envreads live only inlib/config.js.child_process/execFile/spawnlive only inplugins/youtube/youtube.js,plugins/vnc/vnc-launcher.js, andlib/launcher.js.server.jshas the routes but zeroprocess.envand zerochild_process.lib/metrics.jshas noprocess.envand no HTTP method string literals;prom-clientis lazy-loaded only whenPROMETHEUS_ENABLED=1.lib/request-utils.jscontains HTTP method strings but noprocess.env.
When adding features needing env vars or subprocesses, put that code in a lib/ module and import the result into server.js or the plugin's route file.
Plugin register(app, ctx) API, 29-event bus, ctx.createMetric, mutating hooks (browser:launching, session:creating, …), apt.txt + post-install.sh dependency files, and the youtube reference implementation: references/plugin-development.md.
Testing
npm test # unit + e2e + plugin
npm run test:e2e # e2e only
npm run test:live # hits real sites (Google, macros)
npm run test:debug # with server stdout
npm run test:plugins # all plugin tests
npx jest plugins/youtube
Key Files
server.js— Express routes + browser logic (noprocess.env, nochild_process)lib/config.js— all env var readslib/launcher.js— Camoufox subprocess spawninglib/cookies.js— cookie file I/Olib/metrics.js— Prometheus metrics (lazy)lib/request-utils.js—actionFromReq,classifyErrorlib/snapshot.js— accessibility treelib/macros.js— search macro URL expansionlib/plugins.js— plugin loader + event buslib/auth.js— API-key / loopback middlewarelib/persistence.js— atomic storage statelib/inflight.js— inflight request coalescinglib/tmp-cleanup.js— orphaned temp file cleanupcamofox.config.json— plugin load listopenclaw.plugin.json— OpenClaw gateway integration manifest (separate from camofox's own config)plugins/youtube/,plugins/persistence/,plugins/vnc/— bundled pluginsscripts/install-plugin-deps.sh— Docker build hook forapt.txt+post-install.sh
References
| File | When to read |
|---|---|
| api-reference.md | Exact endpoint schemas, query params, request/response bodies |
| deployment.md | Docker/Fly/Railway/Make targets, env var table, logging, yt-dlp install |
| agent-usage.md | Snapshot → ref → click flow, pagination, screenshots, macros usage |
| sessions-cookies-proxy.md | Cookie import, session persistence, proxy/GeoIP, VNC login, storage state reuse |
| plugin-development.md | register(app, ctx) API, events, scanner isolation, metrics, deps files |