Camoufox-only browser runtime for agents.
This runtime combines:
- Agent-friendly browser control (inspect/query/act/wait)
- Low-level observability (events, console, page errors, network, downloads, eval)
- Persistent sessions with profile reuse (
authMode=sharedby default) - Tab lifecycle API (create, navigate, close)
inspectendpoint for paginated interactive targetsqueryendpoint for robust target discovery- Unified
actendpoint with retries and traces - Composable
waitendpoint (all/anyconditions) - Controlled page-context
evalendpoint - Unified event timeline (
/events) including network + console + page errors + action traces - Authenticated
fetchvia browser session cookies - Download capture and save API
- Init script injection per session (
addInitScript) for userscript-style testing
cd ~/dev/camoufox-browser
npm install
npx camoufox-js fetchThis repository exposes skills/camoufox-browser as a Pi package skill.
pi install git:github.com/vinismarques/camoufox-browserWhat pi install does:
- Adds this repo to your Pi
packagessettings - Clones it under Pi's package directory (for example
~/.pi/agent/git/...) - Runs
npm installin the cloned package - Makes the skill available to Pi from
skills/camoufox-browser/SKILL.md
What pi install does not do:
- It does not symlink/copy into
~/.agents/skills(or~/.pi/agent/skills) - It does not start the runtime (
npm start) - It does not guarantee Camoufox binary prefetch (run
npx camoufox-js fetchif needed)
If your tool discovers skills from ~/.agents/skills, clone, install dependencies, and symlink:
git clone https://github.com/vinismarques/camoufox-browser ~/.local/share/camoufox-browser
cd ~/.local/share/camoufox-browser && npm install && npx camoufox-js fetch
mkdir -p ~/.agents/skills
ln -sfn ~/.local/share/camoufox-browser/skills/camoufox-browser ~/.agents/skills/camoufox-browserTo update later:
cd ~/.local/share/camoufox-browser
git pull --ff-only
npm install
npx camoufox-js fetchIf you are developing this repo locally and want it discoverable via shared skill folders:
./scripts/link-skill.sh # link to ~/.agents/skills
./scripts/link-skill.sh --all # also link ~/.pi/agent/skillsnpm start
# default: http://127.0.0.1:9487./scripts/smoke.shTo inject a script that runs on every page load in a session — equivalent to a @grant none
userscript — use the init scripts API.
Scripts can be loaded from a file on disk (recommended for iterative development) or provided inline:
# Register a script from a file on disk
curl -s -X POST http://127.0.0.1:9487/sessions/<sessionId>/init-scripts \
-H "Content-Type: application/json" \
-d '{"path": "/path/to/my-script.user.js"}'
# Or register an inline script
curl -s -X POST http://127.0.0.1:9487/sessions/<sessionId>/init-scripts \
-H "Content-Type: application/json" \
-d '{"script": "console.log(\"hello\");"}'
# List registered init scripts
curl -s http://127.0.0.1:9487/sessions/<sessionId>/init-scripts
# Remove an init script
curl -s -X DELETE http://127.0.0.1:9487/sessions/<sessionId>/init-scripts/<scriptId>File-based scripts are re-read from disk on every page load. This means you can edit the
file and just reload the tab — no re-registration needed. Scripts run at DOMContentLoaded,
so document.head and document.body are always available.
Recommended agent workflow:
- Write the script to a file on disk.
- Register it once via
POST /sessions/:id/init-scriptswith{"path": "..."}. - Edit the file as needed — changes take effect on next page load.
- Reload the tab to verify.
BROWSER_RUNTIME_PORT(default9487)BROWSER_RUNTIME_HOST(default127.0.0.1)BROWSER_RUNTIME_DATA_DIR(default~/.cache/camoufox-browser)CAMOUFOX_HEADLESS(falseby default)CAMOUFOX_OS(macos/linux/windows, auto-detected by default)CAMOUFOX_HUMANIZE(trueby default)CAMOUFOX_ENABLE_CACHE(trueby default)TAB_ACTION_TIMEOUT_MS(default30000)SESSION_TIMEOUT_MS(default 24h)MAX_EVENTS_PER_TAB(default5000)CAPTURE_RESPONSE_BODIES(falseby default)MAX_CAPTURED_BODY_BYTES(default262144)MAX_DOM_CHARS(default220000)MAX_DOM_FALLBACK_REFS(default240)
curl -s http://127.0.0.1:9487/health
curl -s http://127.0.0.1:9487/capabilities# Register from file (re-read from disk on every page load)
curl -s -X POST http://127.0.0.1:9487/sessions/<sessionId>/init-scripts \
-H "Content-Type: application/json" \
-d '{"path": "/path/to/script.user.js"}'
# Register inline
curl -s -X POST http://127.0.0.1:9487/sessions/<sessionId>/init-scripts \
-H "Content-Type: application/json" \
-d '{"script": "document.body.style.background = \"red\""}'
# List
curl -s http://127.0.0.1:9487/sessions/<sessionId>/init-scripts
# Remove
curl -s -X DELETE http://127.0.0.1:9487/sessions/<sessionId>/init-scripts/<scriptId># create session (defaults: persistent=true, authMode=shared)
curl -s -X POST http://127.0.0.1:9487/sessions \
-H "Content-Type: application/json" \
-d '{"persistent":true,"authMode":"shared","profileName":"main","onProfileBusy":"reuse"}'
# list + inspect + close session
curl -s http://127.0.0.1:9487/sessions
curl -s http://127.0.0.1:9487/sessions/<sessionId>
curl -s -X DELETE http://127.0.0.1:9487/sessions/<sessionId>Cookies:
# import cookies
curl -s -X POST http://127.0.0.1:9487/sessions/<sessionId>/cookies \
-H "Content-Type: application/json" \
-d '{"cookies":[{"name":"sid","value":"...","domain":"example.com","path":"/"}]}'
# list cookies (optional ?url=https://example.com)
curl -s "http://127.0.0.1:9487/sessions/<sessionId>/cookies"# create tab
curl -s -X POST http://127.0.0.1:9487/sessions/<sessionId>/tabs \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'
# navigate
curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/navigate \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/inspect \
-H "Content-Type: application/json" \
-d '{"limit":200,"offset":0,"includeScreenshot":false,"includeDom":false}'Returns paginated targets[] with refs/handles, role/name/text, selector hints, and visibility metadata.
curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/query \
-H "Content-Type: application/json" \
-d '{
"target": {"by":"role","role":"button","name":"Save","exact":true},
"filters": {"visible":true},
"limit": 20,
"offset": 0
}'curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/act \
-H "Content-Type: application/json" \
-d '{
"action":"click",
"target":{"by":"ref","ref":"e12"},
"options":{"force":false},
"retry":{"maxAttempts":3,"backoffMs":150,"on":["ELEMENT_INTERCEPTED","STALE_REF"]},
"waitAfter":{
"mode":"all",
"conditions":[{"kind":"networkIdle"}],
"timeoutMs":10000
}
}'Common action values:
click,dispatchClick,clickTexttype,setFieldselect,chooseMenuItempress,scroll,waithover,focus,clear,check,uncheckdrag,upload
target.by options:
refhandleselectorlabelroletext
curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/wait \
-H "Content-Type: application/json" \
-d '{
"mode":"all",
"conditions":[
{"kind":"urlContains","value":"/dashboard"},
{"kind":"textGone","value":"Loading..."}
],
"timeoutMs":15000
}'Supported condition kinds include:
sleepurlurlContainsselectortext(alias:textPresent)goneText(alias:textGone)networkIdle(case-insensitive;networkidlealso works)
curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/eval \
-H "Content-Type: application/json" \
-d '{
"script":"(args) => document.title",
"args": {},
"timeoutMs": 3000
}'With scoped element target:
curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/eval \
-H "Content-Type: application/json" \
-d '{
"target":{"by":"selector","selector":"button.save"},
"script":"(el, args) => el.textContent"
}'curl -s "http://127.0.0.1:9487/tabs/<tabId>/events?since=0&limit=200"Filter by kind:
curl -s "http://127.0.0.1:9487/tabs/<tabId>/events?kind=request,response,action"curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/fetch \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/api/me","responseType":"json"}'curl -s "http://127.0.0.1:9487/tabs/<tabId>/downloads"
curl -s -X POST http://127.0.0.1:9487/tabs/<tabId>/downloads/<downloadId>/save \
-H "Content-Type: application/json" \
-d '{"path":"/tmp/file.pdf"}'Recommended default flow:
- check
GET /healthandGET /capabilities inspectthe pagequeryif target ambiguity exists- execute
actwith retry policy - verify with
wait - use
evalonly for edge cases
Inside BROWSER_RUNTIME_DATA_DIR:
profiles/<profileName>/...persistent browser profile stateartifacts/sessions/<sessionId>/tabs/<tabId>/downloads/...downloaded filesartifacts/sessions/<sessionId>/tabs/<tabId>/network-bodies/...captured response bodies (opt-in)logs/sessions/<sessionId>/tabs/<tabId>/network.jsonlappend-only event log