Skip to main content
Apostate’s source is at github.com/heretic-tech/apostate under GPL-3.0. Browser changes are patches to Chromium 152.0.7977.83. The Python and Node packages, the MCP server, the test suite, the capture tools and this site are in the same repository.

Repository layout

Rules for a change

  1. 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 in navigator.hardwareConcurrency, so the thread pools Chromium sizes from it agree with what a page reads.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
Three rules for the repository itself:
  • Browser changes are patch files in patches/, listed in patches/series. An edit in .workspace/src that 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 of apostate --version, and the --fingerprint-explain output for the launch:
The first command prints the package and browser versions, such as 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 to main 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:
On a clean checkout the validators print:
The whole set took 34 seconds on an Apple M4 Max. When 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:
Three pages are generated. 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.