The Mess
Every time a new committee member joined our organization's project, setup turned into a 15-minute support call. Someone's Node was the wrong major version. Someone's .env was missing. Someone on an M1 Mac hit a binary compiled for x86. The answer was always the same loop: clone, run, fail, screenshot the error, paste it into ChatGPT, get a generic suggestion, try it, fail again.
The README told you what the environment should look like. Nothing told you what was wrong with yours.
The System
The architecture splits into three layers. Probes read your manifests and inspect local state, producing structured findings. Recipes map each finding to a fix command with a safety classification (safe, destructive, or privileged). Commands like scan, fix, and init wire these together into actual workflows.
scan runs all 9 probes in parallel and renders results as a TTY diff. fix goes through each finding one at a time with a y/n/s/q prompt. Safe recipes batch-apply with --yes. Destructive ones still need per-item confirmation. Anything requiring sudo gets printed to stdout for you to run yourself — EnvDoctor never escalates.
init lets maintainers ship a pinned bootstrap script so contributors can run ./envdoctor scan without a global install. Distribution is a curl-installable POSIX script that SHA-256 verifies the binary before installing. GoReleaser builds for four targets: darwin and linux, amd64 and arm64.
Key decisions & tradeoffs
No shell execution from config. The obvious first design was to let .envdoctor.yaml accept a shell: directive for custom checks. Easy to implement, maximally flexible. But running arbitrary shell from a repo you just cloned is exactly how supply chain attacks work. Recipes are a fixed, reviewed library. Config can select from them but can't define new shell. It's the most deliberate thing I left out.
Structural redaction in bundles. Debug bundles are meant to be shareable, but env files contain secrets. The approach is structural: env var names stay (they're the finding), values are never written, $HOME paths are stripped at serialization. It's a schema contract enforced at write time, not a grep pass after the fact.
The consent model in fix. Safe, destructive, and privileged are actual recipe properties, not just UI labels. The classification has to be decided when the recipe is authored, not at runtime. Anything that needs sudo gets printed for you to paste. EnvDoctor never escalates privileges. This constraint meant every recipe in the library had to be classified upfront, enforced in CI.
What I'd do differently
Ship the docs site earlier. The probe reference pages are the most useful thing for someone evaluating whether to use the tool, and they didn't exist until the final days before v0.1.0. A public URL on day one would have made the feedback loop much tighter.
The arch-mismatch probe is too narrow. It only checks Apple Silicon against known x86-only npm packages. A more honest design would cover glibc mismatches on Linux and Windows-only native modules too. I scoped it down to ship, which was the right call for a two-week project, but it's the first thing I'd expand.
The recipe library is the real moat, not the probe engine, and 25 recipes is thin. I'd have spent the second week entirely on recipes if starting over, and opened a contribution path for them before v0.1.0, not after.