* Pair each library release with the browser build it was tested with
Nothing tied a library release to a browser build: `camoufox fetch` took the
newest build in a channel, and a launch used whatever config.json marked
active, so an upgraded library could run a browser it was never tested with,
and an old library would pick up a newer, incompatible browser.
A released package now carries browser-pin.json, naming the browser release
built from the same sources. With it, and no explicit choice by the user:
- fetch installs exactly that build (no prerelease prompt: it is the build
this release was tested with, prerelease or not);
- a launch uses exactly that build, whatever else is installed or active,
and reports it as not installed rather than falling back to another;
- the fetcher's automatic install (TypeScript's first run) takes only it.
An explicit `camoufox set` still wins, with a one-time warning at launch;
`camoufox set --release` returns to the paired build, and `camoufox active`
says which is in use. The checked-in pin is `{}`, so development checkouts
follow their channel as before.
Also: prerelease library versions (0.5.8b1, 0.5.8-beta.1) parse as their
release; they were read as 0.5.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release a prerelease on every tested merge; promote to stable by tag
Every merge to main whose tests pass now publishes a prerelease of all
three artifacts, and pushing vX.Y.Z on a tested main commit promotes it.
- Build and Release runs after Tests on main. It builds the browser only
when its sources changed (ci.browser_inputs.source_digest: every browser
input, not counting the release number). Each build gets the next unused
beta.N on a release commit beside main -- main is protected -- and is
published as a GitHub prerelease, not a draft, with its source digest in
the notes.
- Publish to pypi follows it: <next>bN on PyPI, then Publish to npm puts
<next>-beta.N under the `next` dist-tag. Both are stamped with the browser
release built from the same sources.
- A vX.Y.Z tag is refused unless the commit is on main and `All tests
passed` succeeded on it. The paired browser prerelease then becomes the
stable, latest release (no rebuild, so users get the tested binaries), and
X.Y.Z goes to PyPI and npm `latest`.
The tested commit travels between workflows as an artifact: a workflow_run
is told main's head, so two quick merges would otherwise publish the second,
untested one. ci/release.py holds the planning, stamping and promotion,
unit-tested in ci/tests/test_release.py.
Also fixes two checks that failed the manual release already: vermin
targeted Python 3.8 exactly, against a package that declares ^3.10 and a
code base that needs 3.9, and check-pack compared npm and PyPI prerelease
versions as strings, although each registry spells them differently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test driver-only pull requests against the release paired with their sources
The scope step matched the release tag named by upstream.sh. With release
numbers now allocated per build, that number is a floor, not a release, so
driver-only pull requests would nearly always rebuild, or fetch a build other
than the one their sources produce. It now asks `ci.release paired` for the
release built from exactly this tree's browser sources, and fetch-browser
installs it through the same pin a released package carries.
Documents the release flow in ci/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* pythonlib: replace asyncio.to_thread so the 3.8 vermin gate passes
publish-pypi.yml checks the package with
`vermin . --eval-annotations --target=3.8 --violations camoufox/`, and
asyncio.to_thread (Python 3.9+) in _resolve_proxy_geo failed it, stopping
the 0.5.7 release. loop.run_in_executor does the same off-loop lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pair with releases cut before the digest marker, and test the pairing against the step
The scope step now asks ci.release paired, which only knew releases whose notes
carry a source digest. None does yet: v156.0.1-beta.32, the release built from
main's sources, predates the marker. So every driver-only pull request would have
rebuilt the browser, the first merge would have cut a duplicate beta.33, and the
two scope tests in ci/tests/test_ci.py -- which ran the step in a scratch repo
where ci.release did not import -- failed.
find_paired falls back to the tag upstream.sh names when that release is
published (a prerelease counts; a draft does not) and no browser source changed
since, listing the files that did when they have. browser-plan and promote use
the same lookup. paired takes --root and --releases so the tests run the
workflow's own step against a scratch repo and a fixed release list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release from one workflow, with trusted publishing
The release chain was four workflows linked by workflow_run, with the
tested commit carried between them as an artifact; a browser release
number committed beside main; pairing data in HTML comments in release
notes; a stored PyPI token; and packages rebuilt at each publish.
release.yml now does all of it with `needs`:
- On a push to main it calls tests.yml on the pushed commit (tests.yml
loses its own push trigger), then builds the browser only when its
sources changed, and publishes a library prerelease only when something
a package ships changed. Docs- and CI-only merges publish nothing.
- A browser release's number lives only in its tag, which points at the
tested main commit; `ci.release set-build` writes it into the build's
working tree. Nothing is committed.
- Each browser release carries a manifest.json asset (source digest,
commit), which is what a library pairs by. Builds are attested with
actions/attest-build-provenance.
- Both packages are built once, in build-library, and the publish jobs
upload exactly those files. PyPI and npm use trusted publishing; no
credential is stored.
- A vX.Y.Z tag builds and checks both packages before promoting the
paired browser and publishing.
- Every published library version is tagged (vX.Y.ZbN for a prerelease),
which is how the next merge tells whether the library changed.
- A failed publish is retried with "Re-run failed jobs"; the retry-only
workflow_dispatch path is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Camoufox Python Interface
Lightweight wrapper around the Playwright API to help launch Camoufox.
Note
All the latest documentation is available here.
What is this?
This Python library wraps around Playwright's API to help automatically generate & inject unique device characteristics (OS, CPU info, navigator, fonts, headers, screen dimensions, viewport size, WebGL, addons, etc.) into Camoufox.
It uses fpgen under the hood to generate fingerprints that mimic the statistical distribution of device characteristics in real-world traffic.
In addition, it will also calculate your target geolocation, timezone, and locale to avoid proxy protection (see demo).
Installation
First, install the camoufox package:
pip install -U camoufox[geoip]
The geoip parameter is optional, but heavily recommended if you are using proxies. It will download an extra dataset to determine the user's longitude, latitude, timezone, country, & locale.
Next, download the Camoufox browser:
camoufox fetch
fetch also installs fpgen's model, pinned by sha256, into fpgen's package directory. Otherwise the first generated fingerprint installs it. Run fetch as that directory's owner if the browser will run as another user, e.g. while building a Docker image.
To uninstall, run camoufox remove.
Installing multiple Camoufox versions & from other repos
UI Manager
Manage installed browsers, active version, IP geolocation databases, and package info. Basically a Qt front end for the Python CLI tool.
More updates on it will be coming soon.
To use the gui, install Camoufox with the [gui] extra:
pip install 'camoufox[gui]'
To launch:
camoufox gui
CLI Manager
Demonstration
https://github.com/user-attachments/assets/992b1830-6b21-4024-9165-728854df1473
See help message
$ python -m camoufox --help
Usage: python -m camoufox [OPTIONS] COMMAND [ARGS]...
╭─ Options ─────────────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────────────────────────────╮
│ active Print the current active version │
│ fetch Install the active version, or a specific version │
│ gui Launch the Camoufox Manager GUI (requires PySide6) │
│ list List Camoufox versions │
│ path Print the install directory path │
│ remove Remove downloaded data. By default, this removes everything. │
│ Pass --select to pick a browser version to remove. │
│ server Launch a Playwright server │
│ set Set the active Camoufox version to use & fetch. │
│ By default, this opens an interactive selector for versions and settings. │
│ You can also pass a specifier to activate directly: │
│ Pin version: │
│ camoufox set official/stable/134.0.2-beta.20 │
│ Automatically find latest in a channel source: │
│ camoufox set official/stable │
│ sync Sync available versions from remote repositories │
│ test Open the Playwright inspector │
│ version Display version, package, browser, and storage info │
╰───────────────────────────────────────────────────────────────────────────────────────╯
sync
Pull a list of release assets from GitHub.
> camoufox sync
Syncing repositories...
Official... 24 versions
CoryKing... 2 versions
Synced 26 versions from 2 repos.
Which browser build is used
Each camoufox release is paired with the one browser build it was built and tested with. By default, camoufox fetch installs exactly that build and every launch uses it. That holds even when other builds are installed, and even when the paired build is a prerelease (a prerelease of this package pairs with a prerelease browser). Upgrading the package therefore never runs a browser it was not tested with. Run camoufox fetch after upgrading to install the new pairing.
Choosing a channel or a build with camoufox set overrides the pairing. The choice is kept, and a launch warns that the build differs from the paired one. camoufox set --release goes back to the paired build.
A development checkout (installed from the repository, not from PyPI) is paired with nothing and follows its channel, official/stable by default.
set
Choose a version channel or pin a specific version, overriding the paired build. Can also be called with a specifier to activate directly.
Interactive selector:
> camoufox set
You can also pass a specifier to pin a specific version or choose a channel to follow directly. Following official/stable pulls the latest stable version from the official repo on camoufox fetch:
> camoufox set official/stable
Follow latest prerelease version from the official repo, if applicable:
> camoufox set official/prerelease
Pin a specific version:
> camoufox set official/stable/134.0.2-beta.20
Go back to the build this release is paired with:
> camoufox set --release
active
Prints the current active version string:
> camoufox active # A released package uses its paired build by default
official/prerelease/156.0.1-beta.33 (1a2b3c4d) (paired with this release)
> camoufox set coryking/stable/142.0.1-fork.26
Pinned: coryking/stable/142.0.1-fork.26
Run 'camoufox fetch' to install.
> camoufox active # A specific version is pinned
coryking/stable/142.0.1-fork.26 (not installed)
fetch
Install the browser build this release is paired with, or, after camoufox set, the latest version from the chosen channel. This will also automatically sync repository assets.
> camoufox fetch # Install the paired build (or the latest in the chosen channel)
To download the latest from a different channel, or pin a version:
> camoufox set coryking/stable
> camoufox fetch # Will download the latest release from CoryKing's repo for now on
Or pass in the identifier to download directly without activating it:
> camoufox fetch official/stable/135.0-beta.25 # Install a specific version
list
List installed or all available Camoufox versions as a tree.
> camoufox list # show installed versions
> camoufox list all # show all available versions from synced repos
> camoufox list --path # show full install paths
remove
By default, removes the entire camoufox data directory.
> camoufox remove
> camoufox remove -y # skip confirmation prompt
Remove a specific version:
> camoufox remove official/stable/134.0.2-beta.20
Interactively select a version to remove:
> camoufox remove --select
version
Display the Python package version, active browser version, channel, and update status.
> camoufox version
Python Packages
Camoufox v0.5.7
fpgen v1.3.0
Playwright v1.62.0
Browser
Active official/stable/152.0.4-beta.31
Current browser v152.0.4-beta.31
Installed Yes
Latest in official/stable? Yes
Last Sync 2026-03-07 00:23
GeoIP
Database MaxMind GeoLite2
Updated 2026-03-07 00:24
Storage
Install path /home/name/.cache/camoufox
Browser(s) directory size 1.2 GB
GeoIP database size 40.7 MB
Config file /home/name/.cache/camoufox/config.json
Repo cache /home/name/.cache/camoufox/repo_cache.json
path
Print the install directory path.
> camoufox path
/home/name/.cache/camoufox
test
Open Camoufox with the Playwright inspector for debugging.
> camoufox test
> camoufox test https://example.com
server
Launch a remote Playwright server.
> camoufox server
Usage
All of the latest stable documentation is available at camoufox.com/python.