Repository layout
Rules for a change
- Change the value where Chromium produces it. Find the C++ that produces a value and patch it there. A change that injects a script, sets a DevTools override or redefines a JavaScript getter is not accepted. The core count, for example, is changed in
base::SysInfo::NumberOfProcessors()and not innavigator.hardwareConcurrency, so the thread pools Chromium sizes from it agree with what a page reads. - Serve what the persona claims. A persona that names a GPU gets that GPU family’s WebGL and WebGPU values. A Windows persona gets Windows voices, fonts, system colours and screen layout. When the host cannot back a claim, the persona still serves the claimed value. It never falls back to the host’s value and never serves null. Only host mode shows host values.
- No per-call randomness. Two reads of one value in a session return the same result. The same seed composes the same machine, and a persistent user data directory keeps its machine.
- Measure the change. A change is done when it is measured on the real target: the FingerprintJS Pro suspect score and flags (FingerprintJS), and a probe diff against a capture of the machine the persona claims (Captures). Report the numbers as measured, with the date, the Apostate version, the persona and the host.
- Add no new tell. A change must not add anything a page or the host can see that stock Chrome does not have: a command-line switch a page can read, a new Mojo interface, an unusual process name, a timing change.
- Browser changes are patch files in
patches/, listed inpatches/series. An edit in.workspace/srcthat no patch records is lost the next time the series is applied. Patches has the workflow. - Do not commit a third-party dataset, such as someone else’s fingerprint corpus. Captures in
resources/fingerprints/raw/are of machines the project measured itself. - Keep commits small. The message says what changed and why.
Propose a change
Report a bug or propose a feature in an issue. Include the host OS and CPU, the persona and seed, the output ofapostate --version, and the --fingerprint-explain output for the launch:
apostate 0.4.3 (Chromium 152.0.7977.83).
To send a change, fork the repository, commit on a branch and open a pull request against main. CI runs the checks below on every pull request, but it does not build Chromium. When your pull request changes a patch, say which targets you built and what you measured on them.
Checks on a pull request
Two workflows run on every push tomain and every pull request. Neither builds Chromium.
.github/workflows/check.yml runs eight jobs. None of them needs a Chromium checkout or a browser.
.github/workflows/test-suite.yml runs two jobs against the released browser, not a build of the checkout’s patches:
Run the
check.yml checks from the repository root with Python 3 and Node 22 or later:
sync-packages.py --check reports a stale package copy, run python3 scripts/sync-packages.py to update both packages, and commit the result.
Test suites
The test suite has an offline tier, which uses only local pages, and a live tier, which loads public detector pages. Test suite describes each check, and Run the tests has the options and the live tier.
python3 tests/report.py rewrites Latest results from the files in tests/results/.
scripts/checks/ holds two checks that need a built browser. Both use only Node’s standard library and exit non-zero on a failure.
release-smoke.mjs launches the browser headless for each persona and checks that navigator.platform, the User-Agent and the Sec-CH-UA-Platform header name the persona’s OS, that AudioContext.baseLatency is the persona’s (480, 256 and 512 frames at 48 kHz for Windows, macOS and Linux), that a Thai LANG in the environment does not reach navigator.languages or Intl, and that a persistent user data directory keeps one seed across launches. Pass a persona as the second argument to check only that one.
gl-caps-check.mjs launches the browser once per GPU family with --fingerprint-anchor and checks that the WebGL limits a page reads are the ones measured for that family, that no two families serve identical limits, and that the renderer string is one the catalogue pairs with that family. It opens a window for each launch, so it needs a display. Its arguments are the browser path and the repository root:
Documentation changes
The pages of this site are the.mdx files in docs/, and docs/docs.json lists them. docs/AGENTS.md has the writing rules. Run the checks from docs/ with the Mintlify CLI:
scripts/generate-docs-reference.py writes reference/gpu-models.mdx and reference/font-lists.mdx, and tests/report.py writes testing/results.mdx. Edit the scripts, not those pages.
mint dev serves the site at http://localhost:3000.
Next steps
Build from source
Pinned inputs, host requirements and the build scripts.
Patches
Write a patch, place it in the series and compile it.
Captures
Record a real machine and turn the capture into data.
Releases
Tag a release, check it and publish the packages.