EnvFixer
Root-cause diagnosis for "passes locally, fails in CI."
EnvFixer captures a privacy-safe fingerprint of a Python environment (interpreter, OS/arch/libc, installed packages, and environment-variable names), compares two of them, and ranks the most likely cause of an environment-drift failure — then prints a plain-English fix for each.
Every other tool in this space stops at "here's the diff." EnvFixer tells you which difference is probably the cause, and how to fix it.
$ envfixer diff local.json ci.json
EnvFixer — diagnosis
=====================
3 candidate cause(s), most likely first:
1. [HIGH ] 82/100 ████████████████···· package_missing: pydantic
working: '2.4.2' failing: None
why: A package present where the code works is entirely absent where it
fails; a missing import is the single most common CI failure.
fix: Install 'pydantic' in the failing environment — add
'pydantic==2.4.2' to your requirements/lockfile so CI installs it.
2. [MEDIUM] 58/100 ████████████········ python_version: python
working: '3.11.5' failing: '3.9.18'
...
- Zero runtime dependencies. The core is pure standard library, so it runs inside any minimal CI container.
- Privacy-safe by construction. It records environment-variable names only — never values. The fingerprint is plain JSON you can eyeball.
- A CI guardrail, not just a report. Exit codes and
--fail-onlet it gate a build when environment drift is the likely culprit.
Install
pip install envfixer
Quick start
Capture a fingerprint inside the environment you want to snapshot (run it in
the target virtualenv, e.g. python -m envfixer capture, so you fingerprint
the right interpreter):
# on your machine
python -m envfixer capture -o local.json
# in CI
python -m envfixer capture -o ci.json
Diagnose the difference (first argument = where it works, second = where it fails):
envfixer diff local.json ci.json
Have the failing traceback? Pass it — differences it names are boosted to near-certainty:
envfixer diff local.json ci.json --traceback ci-error.log
Health-check a single environment (no second fingerprint needed) — finds missing dependencies and version conflicts, each with a fix:
envfixer check
No EnvFixer in the failing environment? Diagnose it from its logs. Any
pip freeze output works as an input, so you never have to install anything in
the broken container:
envfixer capture --from-freeze ci-freeze.txt --python-version 3.9.18 -o ci.json
envfixer diff local.json ci.json
Anything the dump cannot tell us (OS, env vars, timezone…) stays unknown and
is skipped when diffing — a partial fingerprint never invents a difference.
Commands
| Command | What it does |
|---|---|
capture [-o FILE] |
Write this environment's fingerprint (stdout by default). |
capture --from-freeze FILE |
Build a fingerprint from a pip freeze/requirements dump (- = stdin). |
diff WORKING FAILING [--traceback FILE] |
Rank the likely causes of the failure. |
check [FINGERPRINT] |
Surface missing dependencies and version conflicts in one environment. |
Shared reporting flags on diff/check:
--format {human,json,markdown,sarif}—markdownfor PR comments,sariffor inline GitHub code-scanning annotations.--fail-on {none,low,medium,high}— exit non-zero when a cause of at least this confidence is found (defaulthigh).--top N— show only the top N results.
Use in CI (GitHub Actions)
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install envfixer
- run: envfixer capture -o ci.json
# compare against a committed baseline captured locally
- run: envfixer diff local.json ci.json --format markdown --fail-on high
A reusable composite action lives in .github/actions/envfixer.
How the ranking works
The ranking is a transparent rule system, not a black box — every score comes with a human-readable rationale. In brief:
- Missing package (present where it works, absent where it fails) scores highest — it's the most common real cause.
- Version mismatches are scored by blast radius (major > minor > patch).
- Python / libc / arch differences are scored by how often they actually break code (e.g. glibc-vs-musl is weighted heavily).
- Timezone and locale/encoding drift is scored too — the UTC-CI date bug
and the ASCII-locale
UnicodeEncodeErrorare real causes no lockfile sees. - Env-var names are filtered by significance:
DATABASE_URL,*_TOKEN,AWS_*,PYTHONPATHscore high; shell noise likeSHLVL/TERMis dropped. - A provided traceback strongly boosts any difference it names.
See envfixer/rank.py for the exact rules.
Evaluation
Because "ranks by likely cause" is an empirical claim, EnvFixer ships a
benchmark that measures it against both a synthetic fault injector and a curated
real-world corpus (missing test plugins, urllib3 v2, Python 3.12 dropping
distutils, Alpine/musl wheel gaps, NumPy 2.0 ABI breaks, UTC timezone drift,
ASCII-locale encoding errors, …). On the current
corpus (23 labelled cases): Top-1 100%, MRR 1.000, 0 false positives. See
EVALUATION.md.
python -m evaluation.benchmark
Privacy
EnvFixer never reads environment-variable values, only their names. The fingerprint is human-readable JSON; inspect it before sharing.
Contributing
Issues and PRs welcome — see CONTRIBUTING.md. Good first
contributions: new fault scenarios in evaluation/generate.py and new remedies
in envfixer/fixes.py.
License
MIT — see LICENSE.
Metadata
Release files for envfixer 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| envfixer-0.2.0.tar.gz | 30.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| envfixer-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.7 kB
Release files / envfixer-0.2.0.tar.gz
| Download URL | envfixer-0.2.0.tar.gz |
|---|---|
| Size | 30.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b63fc8a0811e63dfe96444c6f22fb3b2bc32211e65add587e6d7f946ab7e67d4
|
|
BLAKE2b-256 checksum How to use checksums |
beb9b55d8da9dce54f06188ddd0d92c0c5ec974033fd7cc4f0619750816def44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency logRelease files / envfixer-0.2.0-py3-none-any.whl
| Download URL | envfixer-0.2.0-py3-none-any.whl |
|---|---|
| Size | 26.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
48637a31144625a2e19f1032032d238de4fce3fe1b7b259f8f988f9abe7a5349
|
|
BLAKE2b-256 checksum How to use checksums |
9c76e3875311672ab233da561f8c32b55226e2a93bf0f0bb7e0155581de8e525
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency log