Camoufox: setup, proxies and hCaptcha.
Camoufox is an open-source Firefox build that gives each Playwright launch a new device identity. What it is, who maintains it, how to set it up in Python, and what to do when a site shows hCaptcha.
What Camoufox is
The Camoufox README calls it “an open source anti-detect browser built for webscraping & AI agents”. It is Firefox with patches: on every launch, a launcher draws a complete device from a model of real-world traffic (operating system, screen, fonts, GPU, voices, timezone), checks that the parts agree, and the browser reports those values from its C++ code. Playwright drives it, and the automation layer runs where the page cannot see it.
It comes in three parts, all from the same GitHub repository:
| Part | Install | Latest version | Licence |
|---|---|---|---|
| The browser (a patched Firefox) | camoufox fetch | v158.0.1-beta.41 (Firefox 158.0.1), 29 Oct 2026 | MPL-2.0 |
Python launcher (camoufox on PyPI) | pip install -U "camoufox[geoip]" | 0.5.9, 1 Nov 2026 | MIT |
Node launcher (camoufox on npm) | npm install camoufox playwright-core | 0.5.9, 1 Nov 2026 | MIT |
The repository had about 13,000 stars on 2 November 2026. The docs site is camoufox.com, and the packages are on PyPI and npm.
Who maintains it, and how current it is
The repository and the PyPI package are published under daijro, who created the project in July 2024. Most of the work now comes from other contributors: of the 173 commits in October 2026, JWriter20 made 126 and coffeegrind123 made 28, and the npm package is published by jwriter20. Releases go out on a twice-monthly train; October 2026 brought the move from Firefox 156 to 158.
The project calls itself unfinished. The README opens with a warning:
“This project is under development. It may not be suitable for stable
production use.” Its current browser releases carry beta version
numbers. Clover Labs,
one of the project’s sponsors, keeps a fork that it publishes as
cloverlabs-camoufox on PyPI (0.6.0, May 2026); the
camoufox package above is the main project’s.
Install it in Python
pip install -U "camoufox[geoip]"
camoufox fetch camoufox fetch downloads the browser build the package release
is paired with, plus the fingerprint model and, with the
[geoip] extra, an IP location database. Skip it and the first
launch downloads them instead. camoufox version
prints what is installed, and camoufox remove
deletes it all. It needs Python 3.10 or newer. The package installs its
own Playwright, pinned below 1.63, so you do not run
playwright install.
We installed it on 2 November 2026 in a Python 3.13 virtualenv: it pulled
Playwright 1.62.0, and camoufox fetch installed browser
v158.0.1-beta.41. The same pip install on Python 3.15 failed
while building one of its dependencies, zstandard.
Python quick start with Playwright
Camoufox replaces Playwright’s launch call and hands back a
normal Playwright Browser:
from camoufox.sync_api import Camoufox
with Camoufox(headless=True) as browser:
page = browser.new_page()
page.goto("https://example.com")
print(page.evaluate("navigator.webdriver")) # False
print(page.evaluate("navigator.userAgent")) # a Firefox 158 user agent
We ran it headless twice. Both runs read navigator.webdriver
as false. The first launch drew a Linux machine (user agent
X11; Linux x86_64; rv:158.0, a
1920×1080 screen), the second a Mac
(Macintosh; Intel Mac OS X 10.15,
1512×982), so each launch really is a different device. Stock Playwright
Firefox in the same virtualenv read navigator.webdriver as
true.
The first launch also printed a LeakWarning: when the drawn
identity claims another operating system than the host’s, Camoufox makes
canvas and WebGL readback random, which reads as a privacy-hardened
Firefox. Pass your host’s OS (os="macos" on a Mac,
os="linux" on Linux) to keep stock canvas behaviour, or canvas_noise=False.
headless=False is the default and opens a window, and on Linux
headless="virtual" runs a headed browser on a hidden Xvfb
display.
The async API is the same with AsyncCamoufox:
import asyncio
from camoufox.async_api import AsyncCamoufox
async def main():
async with AsyncCamoufox(headless=True) as browser:
page = await browser.new_page()
await page.goto("https://example.com")
asyncio.run(main()) Node quick start
// npm install camoufox playwright-core && npx camoufox fetch
import { Camoufox } from "camoufox";
const browser = await Camoufox({ headless: true });
const page = await browser.newPage();
await page.goto("https://example.com");
await browser.close();
It needs Node 22.15 or newer and playwright-core
below 1.63. We ran it the same day with
playwright-core 1.62.1: same browser build,
navigator.webdriver false, a Windows Firefox 158
user agent.
Proxy setup
The README is direct about it: “Camoufox is intended to be used with
rotating proxies (preferably residential IPs).” Pass Playwright’s proxy
dict and add geoip=True. The
launcher then looks up the exit IP through the proxy and sets timezone,
locale, geolocation and the WebRTC IP to match it, so the browser does not
claim Berlin while its traffic leaves from Dallas. This example is adapted
from the Camoufox Python README:
from camoufox.sync_api import Camoufox, NewContext
with Camoufox(
headless=True,
proxy={"server": "http://proxy.example.com:8080", "username": "user", "password": "pass"},
geoip=True,
) as browser:
page = browser.new_page()
page.goto("https://example.com")
# A second identity with its own proxy, in the same browser:
other = NewContext(
browser,
proxy={
"server": "http://proxy2.example.com:8080",
"username": "user",
"password": "pass",
},
)
other.new_page().goto("https://example.com") NewContext gives each context its own identity and, if you
pass one, its own proxy, so one browser can carry several. With
geoip=True and no proxy, Camoufox keeps IPv6 on only when your
machine’s IPv6 address places you where its IPv4 address does; otherwise
the browser behaves as if on an IPv4-only network.
One Playwright difference: the isolated world
Camoufox runs Playwright’s evaluate, selectors and init
scripts in an isolated copy of the page, so the page never sees them. The
DOM is shared, so reading elements and setting a field’s value work as
usual. The page’s own JavaScript variables and functions are not visible
from there. To reach them, launch with
main_world_eval=True and prefix
the script with mw::
with Camoufox(headless=True, main_world_eval=True) as browser:
page = browser.new_page()
page.goto("https://your-site.example/")
# 'undefined': the page's own functions are not visible here
page.evaluate("typeof window.onSolved")
# 'function': mw: runs in the page's own world
page.evaluate("mw:typeof window.onSolved")
That matters for hCaptcha. Many Playwright examples, including
ours, finish by calling the widget’s
data-callback function with
window[name](token). In Camoufox, our test of that call raised
“window.onSolved is not a function”; the same call with
mw: ran the page’s callback.
What its own docs say it does not do
In the project’s words, as read on 2 November 2026:
- Not production-ready. “This project is under development. It may not be suitable for stable production use.”
- Not undetectable. “Camoufox can still be detected. Thousands of values must agree with each other, and anti-bot vendors look for the one that does not.” Site-specific reports are collected in one tracking issue.
- Firefox only. “Camoufox presents Firefox identities only. Some WAFs test SpiderMonkey engine behaviour, which no amount of spoofing turns into Chromium.”
- Not your IP. Its leak-debugging chart starts by checking whether the site also flags stock Firefox; if it does, the verdict is “Not the browser: a bad IP or rate limiting”.
- No CAPTCHA solving. The only CAPTCHA the Python README
names is Turnstile, in the
disable_coopoption, which lets Playwright click the Turnstile checkbox inside its cross-origin frame. Neither the main README nor the Python README mentions hCaptcha.
Does Camoufox get past hCaptcha?
Camoufox changes what your browser says about itself. That is one of several things that decide whether a site’s hCaptcha shows a challenge at all; why am I getting hCaptcha covers the others, starting with the reputation of your IP address.
Once a challenge does appear, Camoufox has nothing more to offer. The site
accepts the form only with a valid token in the
h-captcha-response field, a
P1_ string its server checks with hCaptcha before it lets the
request through. Invisible hCaptcha needs that token too, even when no
puzzle is shown. A browser, however well disguised, does not produce one.
Get an hCaptcha token in a Camoufox session
NoneCap is an hCaptcha solving API. Keep Camoufox for the rest of your run,
and when a page has hCaptcha, send its sitekey and
url to NoneCap, get a P1_ token back, put it in
the form and submit, or call the widget’s callback through
mw:. Failed, cancelled and expired solves are never charged.
import os
import time
import requests
from camoufox.sync_api import Camoufox
API = "https://api.nonecap.com/v1/solves"
HEADERS = {"Authorization": f"Bearer {os.environ['NONECAP_KEY']}"}
# One sticky proxy session for the browser and the solve.
PROXY = {
"host": "proxy.example.com",
"port": 8080,
"username": "user",
"password": "pass",
}
CAMOUFOX_PROXY = {
"server": f"http://{PROXY['host']}:{PROXY['port']}",
"username": PROXY["username"],
"password": PROXY["password"],
}
def solve_hcaptcha(sitekey: str, url: str, user_agent: str) -> str:
r = requests.post(
API,
headers=HEADERS,
params={"wait": 90},
json={
"type": "hcaptcha",
"sitekey": sitekey,
"url": url,
"user_agent": user_agent,
"proxy": PROXY,
},
timeout=120,
)
r.raise_for_status()
data = r.json()
while data["status"] in ("pending", "solving"):
time.sleep(2)
data = requests.get(f"{API}/{data['id']}", headers=HEADERS, timeout=30).json()
if data["status"] != "solved":
raise RuntimeError(f"solve {data['status']}: {data.get('error')}")
return data["token"]
with Camoufox(
headless=True,
proxy=CAMOUFOX_PROXY,
geoip=True,
main_world_eval=True,
) as browser:
page = browser.new_page()
page.goto("https://your-site.example/signup")
widget = page.locator("[data-sitekey]").first
sitekey = widget.get_attribute("data-sitekey")
callback = widget.get_attribute("data-callback")
token = solve_hcaptcha(sitekey, page.url, page.evaluate("navigator.userAgent"))
page.evaluate(
"""(token) => {
for (const name of ['h-captcha-response', 'g-recaptcha-response']) {
const el = document.querySelector(`textarea[name="${name}"]`);
if (el) el.value = token;
}
}""",
token,
)
if callback:
page.evaluate(f"mw:(token) => window['{callback}'](token)", token)
else:
page.click("button[type=submit]")
Two optional fields carry the browser’s context into the solve:
user_agent, the user agent of the browser that loaded the
page, which the API reference says some sites need for the token to be
accepted; and proxy, so the solve leaves from the same address
as your Camoufox session. Some sites reject a token submitted from a
different address, so use one sticky session for page load, solve and
submit; the hCaptcha proxy guide
explains when that matters. Leave proxy out and the solve runs
from NoneCap’s own pool.
The Playwright guide covers invisible
and enterprise (rqdata) sitekeys and how to tell hCaptcha from
reCAPTCHA and Turnstile in the DOM, and every field and status is in the
API reference. NoneCap solves
hCaptcha only; it does not help with reCAPTCHA, Turnstile
or other CAPTCHAs.
Camoufox alternatives
If you need Chromium rather than Firefox, the options patch Chrome instead of building a new browser:
- playwright-stealth
injects JavaScript patches into each page of an ordinary Playwright
browser. The README’s comparison table sets this against Camoufox: page
overrides show up in
toString()and are often missed in workers and iframes. - Patchright is a patched Playwright driver. By its own README, it “only patches CHROMIUM based browsers. Firefox and Webkit are not supported.”
- Antidetect profile browsers such as AdsPower are desktop apps with saved profiles rather than libraries; the AdsPower guide covers hCaptcha there.
None of these returns an hCaptcha token either; the solve step above works the same with each of them.
Last updated November 2026.
Frequently asked
What is Camoufox?
camoufox package for Python or Node.Is Camoufox free and open source?
daijro/camoufox.Who maintains Camoufox?
Does Camoufox work with Playwright?
Camoufox(...) returns an ordinary Playwright Browser, so existing Playwright code works once the launch line changes. The packages pin Playwright below 1.63. One difference: page.evaluate runs in an isolated world, so to call a function the page defined you launch with main_world_eval=True and prefix the script with mw:.How do I use a proxy with Camoufox?
proxy={"server": ..., "username": ..., "password": ...}, and add geoip=True so timezone, locale, geolocation and the WebRTC IP match the proxy’s exit IP. geoip needs the [geoip] extra. NewContext(browser, proxy=...) gives one context its own proxy.Is camoufox-js the same as Camoufox for Node?
camoufox-js is Apify’s separate “Experimental JS port of Camoufox for Python” (0.13.0, October 2026). The Camoufox project now publishes its own Node package as camoufox on npm.Does Camoufox solve CAPTCHAs?
h-captcha-response. NoneCap returns that token for hCaptcha.