The idea
A single-binary CLI — call it envdiff — that walks the machine it runs on and writes a canonical fingerprint of everything that can plausibly change program behaviour: kernel and libc versions, CPU features, locale and timezone, shell, resolved toolchain versions, the contents of language lockfiles, relevant environment variables, and the dynamic libraries each project binary actually links against. The output is one deterministic JSON file. The second command, envdiff compare mine.json ci.json, prints a ranked list of differences with the noise collapsed, so "works on my machine" becomes a diff you can read in ten seconds.
Why build this
Every team loses hours a month to a build or test that passes in one place and fails in another, and the hunt is almost always manual: paste node -v into Slack, discover someone's on glibc 2.35, find the stray NODE_OPTIONS in a shell profile, notice CI runs in UTC while the failing developer is on Europe/Berlin. The information needed to settle it is cheap to collect and nobody collects it, because collecting it by hand is tedious and the output is unreadable without something to compare it to.
Two things make now a reasonable time. Toolchains have become version-pinned and introspectable — rustup, uv, mise, asdf, Corepack and the rest all report resolved versions in machine-readable form, so a fingerprint no longer means scraping --version strings. And nearly every CI provider will upload a JSON artifact for free, which gives you the other half of the diff without asking anyone to install anything.
Stack sketch
- Rust, distributed as a static binary via
cargo-dist— the tool has to run on a broken machine, so no runtime dependency. sysinfofor host facts,os_releaseparsing for the distro,nixfor locale and resource limits.goblinto read ELF and Mach-O headers and list required shared libraries and their versions, rather than shelling out toldd/otool.- Lockfile parsers for the common ecosystems:
cargo_metadata, plus plain serde structs forpackage-lock.json,uv.lock,poetry.lock,go.sum. - RFC 8785 JSON canonicalization so the same machine always produces byte-identical output and fingerprints can be hashed and cached.
similarfor the structural diff,ratatuifor an optional interactive view of a large one.- A GitHub Action wrapper that runs
envdiff captureand uploads the result as an artifact, so CI fingerprints exist without per-repo work.
Scope for v1
envdiff captureandenvdiff compare, Linux and macOS only.- A rule file that marks fields as significant, informational, or ignored, with sensible defaults — paths and hostnames should never show up as differences.
- Secret redaction on environment variables: allowlist by name, drop everything else, no heuristic sniffing.
- Deliberately out of v1: Windows, remote fetching of a peer's fingerprint, any server component, any attempt to say which difference caused the failure.
Where it could go
The obvious next step is bisection across environments instead of commits. If a repository accumulates fingerprints from passing and failing runs, the tool can rank candidate fields by how well they separate the two sets — not causation, but a far better starting point than a flat diff. That turns a debugging aid into something closer to a flake triage tool.
After that, the fingerprint becomes a contract. A committed env.expected file lets the tool fail a developer's first build with "your glibc is older than this project supports" instead of letting a confusing linker error happen twenty minutes later. The same file is a genuinely useful input to a Nix flake or devcontainer definition for teams that eventually want full reproducibility but can't start there.
Watch out for
Environment variables are where the secrets live, so redaction has to be allowlist-only and obvious in the output — one leaked token in a CI artifact ends the tool's credibility. The other risk is diff noise: if the default rules surface fifty meaningless differences, nobody reads the fifty-first.