Install
openclaw skills install @zenaufa/camoufox-browseDrive a real Firefox browser via Camoufox for sites with aggressive bot-detection or fingerprinting, only when the built-in browser tool is blocked and the user has confirmed automated access is permitted. Not for general or routine browsing.
openclaw skills install @zenaufa/camoufox-browseDrive real browser sessions through Camoufox when a site's aggressive bot-detection or fingerprinting blocks the built-in browser tool and the user has confirmed automated access is permitted.
Two entry points — pick one:
Node (camoufox-js) | Python (camoufox) | |
|---|---|---|
| Install | npm install only | pip install + python3 -m camoufox fetch |
| Binaries required | node, npm | python3 |
| API | Playwright (sync wrapper) | Playwright (async or sync) |
| Status | experimental (Apify port, v0.11.x) | upstream original |
| Best for | Node-first workspaces, CI, OpenClaw scripts | legacy scripts, Python-first stacks |
Both expose the same Playwright API surface, so the rest of this skill applies to either.
# In your project directory (or globally with -g)
npm install playwright@1.60.0 camoufox-js
# First run auto-fetches the camoufox Firefox binary into node_modules
Headed mode also needs a display server:
xvfb (apt install xvfb) and wrap in xvfb-run, or use headless: true.⚠️ Pin
playwright@1.60.0. Playwright 1.61+ sends aviewport.isMobilefield that camoufox's bundled Firefox rejects onnewPage(), breaking every page open.
playwright-core@1.60.0 + Firefox + camoufox has a bug: when a page raises an uncaught error without a location (common on heavy JS sites like TikTok, social feeds, ad-laden pages), the PageError dispatcher crashes the entire browser at coreBundle.js:49624 reading pageError.location.url. The browser process dies mid-session and you lose all pages and tabs.
Apply this patch to node_modules/playwright-core/lib/coreBundle.js after every npm install:
# Idempotent — safe to re-run. Patches both occurrences (browser context + page).
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("node_modules/playwright-core/lib/coreBundle.js")
src = p.read_text()
old = """ location: {
url: pageError.location.url,
line: pageError.location.lineNumber,
column: pageError.location.columnNumber
}"""
new = """ location: pageError.location ? {
url: pageError.location.url,
line: pageError.location.lineNumber,
column: pageError.location.columnNumber
} : { url: "", line: 0, column: 0 }"""
n = src.count(old)
if n != 2:
raise SystemExit(f"expected 2 occurrences, found {n} — playwright version may have changed")
p.write_text(src.replace(old, new))
print(f"patched {n} occurrences OK")
PY
Verify the patch is in place before any non-trivial run:
grep -c "pageError.location ?" node_modules/playwright-core/lib/coreBundle.js # must print 2
Without this patch, multi-page sessions on TikTok / Instagram / ad-heavy sites will silently die after the first page error. The patch is local to node_modules — npm install overwrites it, hence "after every install."
# Use --break-system-packages on Debian/Ubuntu, or a venv
python3 -m pip install "camoufox[geoip]" playwright==1.60.0
# Fetch the camoufox browser binary (~700 MB) + uBlock Origin addon
python3 -m camoufox fetch
Same playwright==1.60.0 pin applies here. Headed mode requires DISPLAY or xvfb-run. The Python binding uses Playwright's async API directly and does not exhibit the same pageerror crash (as of camoufox 0.4.x); if the Node path is unstable, fall back to Python.
Before any headed run, confirm X11 is actually running. A DISPLAY=:0 env var with no X server underneath is a common failure mode on freshly-booted VMs.
echo "DISPLAY=$DISPLAY"
xdpyinfo -display "${DISPLAY:-:0}" 2>&1 | head -3
# Expect: "name of display: :0" + a vendor string
# If "xdpyinfo: unable to open display" → no X server, install xvfb or use headless.
If xdpyinfo reports a display, headed camoufox runs without xvfb. Otherwise start a virtual display (xvfb-run …) or set headless: true.
// CommonJS
const { Camoufox } = require('camoufox-js');
(async () => {
const browser = await Camoufox({
headless: false, // false for visible window
humanize: true, // human-like mouse movement
geoip: true, // match timezone/locale to exit IP
locale: 'id-ID', // match target site
os: 'linux', // pin to keep fingerprint stable across runs
});
const page = await browser.newPage(); // Firefox Browser exposes newPage directly
await page.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 30000 });
console.log(await page.title());
await browser.close();
})();
First run takes ~10-20s while
camoufox-jsfetches its bundled Firefox (~700 MB, cached innode_modules/camoufox-js/.cacheafter). Subsequent runs are fast.
const browser = await Camoufox({ headless: false, humanize: true, geoip: true });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 30000 });
// Keep window open until user closes it
await new Promise(r => browser.on('disconnected', r));
# Headless server: provide a virtual display
xvfb-run node your_script.js
import asyncio
from camoufox import AsyncCamoufox
async def browse(url: str):
async with AsyncCamoufox(
headless=True, # set False for a visible window
humanize=True, # human-like mouse movement
geoip=True, # match timezone/locale to exit IP
) as browser:
page = await browser.new_page()
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
return await page.title()
print(asyncio.run(browse("https://example.com")))
For synchronous scripts, from camoufox import Camoufox provides the same API without async/await.
Set headless=False to watch the browser drive itself. On a desktop it just works; on a headless server, wrap the script in xvfb-run.
import asyncio
from camoufox import AsyncCamoufox
async def browse_headed(url: str):
async with AsyncCamoufox(
headless=False, # visible window
humanize=True,
geoip=True,
) as browser:
page = await browser.new_page()
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
input("Press Enter to close…") # keep the window open
asyncio.run(browse_headed("https://example.com"))
# Headless server: provide a virtual display
xvfb-run python your_script.py
| Use this skill | Use the built-in openclaw browser |
|---|---|
| Sites with aggressive bot detection | Normal page reads, documentation lookups |
| Tasks where fingerprint consistency matters (logged-in sessions across runs) | Quick fetches, snapshots, form interactions |
| Anything where being identified as a bot would break the task | Bulk page content extraction |
| User explicitly requests a real / anti-fingerprinting browser for an authorized task | When speed matters more than stealth |
Rule of thumb: Only reach for Camoufox after the built-in tool has actually been blocked or detected, or when the user explicitly requests it for an authorized task. Default to the built-in tool otherwise.
Playwright API is identical in both languages; only syntax differs.
// Node
const content = await page.content(); // raw HTML
const text = await page.innerText('body'); // visible text
const title = await page.title();
await page.screenshot({ path: '/tmp/shot.png', fullPage: true });
# Python
content = await page.content()
text = await page.inner_text("body")
title = await page.title()
await page.screenshot(path="/tmp/shot.png", full_page=True)
// Node
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', '...');
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
# Python
await page.fill('input[name="email"]', "user@example.com")
await page.fill('input[name="password"]', "...")
await page.click('button[type="submit"]')
await page.wait_for_load_state("networkidle")
Common multi-step operations. Python shown below; Node mirrors it with camelCase method names (waitForLoadState, querySelector, etc.).
Fill a whole form, then submit:
fields = {
'input[name="first"]': "Ada",
'input[name="last"]': "Lovelace",
'input[name="email"]': "ada@example.com",
}
for selector, value in fields.items():
await page.fill(selector, value)
await page.click('button[type="submit"]')
await page.wait_for_load_state("networkidle")
Navigate, wait, and snapshot in one go:
async def navigate_and_read(page, url):
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
await page.wait_for_load_state("networkidle")
return await page.inner_text("body")
Scroll to reveal below-the-fold / lazy-loaded content:
async def scroll_to_bottom(page, steps=10, pause=0.5):
for _ in range(steps):
await page.mouse.wheel(0, 2000)
await page.wait_for_timeout(int(pause * 1000))
return await page.content() # now includes lazy-loaded items
Click through many elements sequentially:
selectors = ["#accept", "button.load-more", "a.next"]
for sel in selectors:
el = await page.query_selector(sel)
if el:
await el.click()
await page.wait_for_load_state("networkidle")
// Node — camoufox-js returns a Browser; use its default context, not browser.contexts()[0]
const fs = require('fs');
const browser = await Camoufox({ headless: false });
const context = browser.contexts()[0]; // or `await browser.newContext()` for a fresh one
const page = await context.newPage();
// ... do stuff ...
const state = await context.storageState();
fs.writeFileSync('/path/to/state.json', JSON.stringify(state));
await browser.close();
Note on the Node API shape:
camoufox-jsreturns a FirefoxBrowserobject.Browser.newPage()is available directly; for storage state, usebrowser.contexts()[0](one default context is created at launch). The Python binding'sbrowser.new_context()has no direct Node equivalent — use the default context orbrowser.newContext().
# Python
async with AsyncCamoufox(headless=True) as browser:
context = await browser.new_context(storage_state="/path/to/state.json")
# ...
state = await context.storage_state() # save for next run
// Node
const browser = await Camoufox({
headless: true,
proxy: { server: 'http://resi.example.com:8000', username: 'u', password: '***' },
});
# Python
async with AsyncCamoufox(
headless=True,
proxy={"server": "http://resi.example.com:8000", "username": "u", "password": "***"},
) as browser:
...
Useful when a target rate-limits by IP. Only for sites you're authorized to access at volume.
For any run that opens more than one page (scanning event listings, scraping search results, batch-checking URLs), you must isolate errors. Without this, one bad page will tear down the whole browser session.
// Node — minimum-viable wrapper. Apply the Playwright patch FIRST.
const { Camoufox } = require('camoufox-js');
// Belt + suspenders: the patch above stops the dispatcher crash, but
// page-level `pageerror` events still fire on every bad page. Suppress them
// so they don't show up as console noise during the run.
process.on('uncaughtException', e => {
if (String(e?.message).includes("undefined (reading 'url')")) {
// dispatcher crash — already handled by the patch, this is a leftover
return;
}
console.error('UNCAUGHT', e);
});
(async () => {
const browser = await Camoufox({ headless: false, humanize: true, geoip: true });
browser.on('pageerror', () => {}); // silence page-error events
const page = await browser.newPage();
page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(25000);
for (const [slug, url] of STEPS) {
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForTimeout(3000); // let JS render
await page.screenshot({ path: `${slug}.png` });
// ... extract data ...
} catch (e) {
console.error(' ✗', slug, e.message.split('\n')[0]);
// continue to next URL — do not throw
}
}
await browser.close();
})();
The Python binding doesn't need this dance — async error handling is natural there.
| Option | Default | Notes |
|---|---|---|
headless / headless: | True / true | Set false for a visible window. Requires a display server. |
humanize / humanize: | True / true | Human-like mouse movement; turn off for speed. |
geoip / geoip: | True / true | Match timezone/locale to exit IP — keep on for fingerprint consistency. |
locale / locale: | "en-US" | Override per target site. |
os / os: | auto-rotated | Force "windows", "macos", or "linux" if a site targets a platform. |
block_images / block_images: | False / false | Set True for faster loads on image-heavy sites. Triggers a iKnowWhatImDoing warning; pass iKnowWhatImDoing: true to silence. |
proxy / proxy: | None / null | Dict with server, optional username/password. |
fonts / fonts: | auto | List of fonts available in the spoofed OS; rarely needs override. |
navigator.webdriver — camoufox handles this at the C++ level.os and locale deterministically.| Symptom | Likely cause | Fix |
|---|---|---|
Error: Failed to connect to camoufox (Python) | Binary missing | python3 -m camoufox fetch |
| First Node run hangs / fails downloading | camoufox-js fetching bundled Firefox | Wait — one-time ~700 MB download; cached after |
NotInstalledGeoIPExtra (Python) | Missing optional dep | pip install "camoufox[geoip]" |
display: cannot open / xdpyinfo fails | No X server, even though $DISPLAY is set | Install Xvfb / GDM, or set headless: true. See "Headed setup" above. |
Target page is missing / newPage() fails on Node | Playwright 1.61+ | Pin playwright@1.60.0 and reinstall |
Browser process dies after 2-3 pages with Cannot read properties of undefined (reading 'url') | Playwright Firefox pageerror dispatcher bug | Apply the patch in "Required patch" above |
Blocking image requests has been reported to cause detection issues | block_images: true warning | Pass iKnowWhatImDoing: true in launch options |
| Browser opens but pages 403/429 | IP reputation | Add a proxy: with residential IPs |
| Site detects bot anyway | Fingerprint inconsistency across navigations | Stay in one context, don't recreate mid-session |
| Slow first launch (~10-20s Node, ~3-5s Python) | Normal — booting Firefox profile | Wait it out |
Anti-detect browsing touches real accounts, files, and destructive actions. Keep runs safe:
storage_state/profile per task over your everyday one.Camoufox is a privacy and anti-fingerprinting tool. It is not a license to break any site's terms of service, scrape copyrighted content, evade bans, or impersonate real users. If a site's terms say no automated access, the human — not this skill — decides whether to proceed. Surface the question; don't make the call silently.
This skill drives Camoufox, the anti-detect Firefox project licensed under MPL-2.0. It is installed by the user from PyPI (pip install camoufox) or npm (npm install camoufox-js) and is not bundled or redistributed here.
camoufox-js — Apify's experimental JS port.camoufox — upstream original.This skill only provides original instructions and example code; it is published under MIT-0. Camoufox's own license and terms apply to the browser you install.