Local-first consistency checker for environment variables. Detects missing, unused, duplicate, and mismatched variables across .env files, Docker Compose, Kubernetes manifests, GitHub Actions, and source code โ no network calls, no telemetry, no values ever printed.
Available for six ecosystems โ Node, Python, Go, Ruby, PHP, and Java โ as native ports, each installable from its own package manager.
๐ Documentation: arun-skg.github.io/envdoctor โ full guides, per-language references, and examples.
Auto-refreshed daily by the Downloads chart workflow. Click for interactive history on npm-stat.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ENVIRONMENT AUDIT โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ Missing (error) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ COMPOSE_ONLY docker-compose.yml:9 referenced but โ
โ not defined in any environment file โ
โ โ NEW_FEATURE_FLAG src/index.ts:5 used in source โ
โ code but not defined in any โ
โ environment file โ
โ โ
โ Unused (warning) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ DEBUG_MODE .env:7 defined but never โ
โ referenced anywhere โ
โ โ
โ Duplicates (error) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ NODE_ENV .env:2,12 defined 2 times โ
โ โ
โ Type mismatch (error) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ PORT expected: integer ยท found: string โ
โ โ
โ Summary: 8 files scanned ยท 15 variables ยท 4 errors ยท 16 warns โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- Why
- Supported formats
- Installation
- Native ports
- Quick start
- Detectors
- Commands
- Configuration
- Environment labels
- Output formats
- CI integration
- Security
- Architecture
- Development
- Contributing
- License
Environment drift is a silent class of bug: a variable is used in code but never
documented, defined in .env but dead, present in development but forgotten in
production, or a secret accidentally shipped to the client bundle behind a
NEXT_PUBLIC_ prefix. envdoctor reconciles every place a variable can appear โ
.env files, Docker Compose, Kubernetes manifests, GitHub Actions, and your
source code โ into one normalized model, then runs a suite of detectors over it.
It is local-first: everything runs on your machine, nothing is uploaded, and variable values are never printed or written into generated artifacts.
| Source | What is read |
|---|---|
| dotenv |
.env, .env.local, .env.production, .env.*
|
| Docker Compose |
environment: keys and ${VAR} interpolation |
| Kubernetes |
env:, envFrom:, ConfigMap/Secret manifests |
| GitHub Actions | workflow env:, secrets.*, vars.*
|
| Source code |
process.env.X / import.meta.env.X (.ts/.tsx/.js/.jsx/.mjs/.cjs) |
# From npm
npm install -g @arunskg/envdoctor
# Or run directly with npx
npx @arunskg/envdoctor scanUsing another language? See native ports below.
envdoctor is published as a standalone native implementation for each
ecosystem โ no Node required, no wrappers. Every port scans its own language's
environment idioms, reconciles them against your .env files, and exits 1 on
errors so it drops straight into CI.
| Ecosystem | Install | Detects |
|---|---|---|
| Node (reference) | npm install -g @arunskg/envdoctor |
process.env.X, import.meta.env.X
|
Python (python/) |
pip install arun-envdoctor |
os.getenv, os.environ[...], os.environ.get
|
Go (go/) |
go install github.com/arun-skg/envdoctor/go/cmd/envdoctor@latest |
os.Getenv, os.LookupEnv
|
Ruby (ruby/) |
gem install envdoctor |
ENV["X"], ENV.fetch("X")
|
PHP (php/) |
composer require --dev arun-skg/envdoctor |
getenv, $_ENV, $_SERVER
|
Java (java/) |
io.github.arun-skg:envdoctor |
System.getenv("X") |
Perl (perl/) |
cpanm App::Envdoctor (pending CPAN release)
|
$ENV{X} |
All ports share the same CLI shape:
envdoctor scan --dir . # audit; exit 1 on errors
envdoctor scan --strict # treat warnings as errors tooNote: the Python distribution is named
arun-envdoctoron PyPI (the bare name is blocked as too similar to an existing project), but the installed command and importable package are bothenvdoctor.
Each port has its own README, test suite, and CI workflow, and they are kept
behaviour-identical โ the same project produces byte-for-byte-equivalent
findings (and --json output) in every language. The native ports are now at
full feature parity with the Node reference: all ten detectors, the
scan / diff / sync / init / fix subcommands, --json output, and
Docker Compose / Kubernetes / GitHub Actions scanning.
| Detector | Node | Python ยท Go ยท Ruby ยท PHP ยท Perl ยท Java |
|---|---|---|
| missing / undefined-in-source | โ | โ |
| unused | โ | โ |
| duplicates | โ | โ |
| public-prefix (secret leak) | โ | โ |
| weak-secret | โ | โ |
| typo (did-you-mean) | โ | โ |
| environment-diff | โ | โ |
| type-mismatch | โ | โ |
| schema-validation | โ | โ |
--json output |
โ | โ |
scan ยท diff ยท sync ยท init ยท fix
|
โ | โ |
| Docker Compose ยท Kubernetes ยท GitHub Actions sources | โ | โ |
Schema validation reads an envdoctor.schema.json at the project root, e.g.:
{
"PORT": { "type": "integer", "min": 1, "max": 65535 },
"LEVEL": { "enum": ["debug", "info", "warn", "error"] },
"API": { "type": "url" },
"TOKEN": { "type": "string", "optional": true }
}Note: run
npx @arunskg/envdoctorfrom your project directory, not from inside a checkout of this repo โ npx resolves the local package first, whoseenvdoctorbin isn't on your PATH, and you'll seeenvdoctor: command not found. After a global install, the shortenvdoctorcommand works anywhere.
# Bootstrap config + .env.example + ENVIRONMENT.md in your project
envdoctor init
# Scan for issues (exits 1 on errors, 0 on clean)
envdoctor scan
# Compare two environments
envdoctor diff development production
# Copy missing keys from .env to .env.local
envdoctor sync development local
# Scan only files changed on this branch
envdoctor scan --since HEAD
# Generate/update docs (dry-run first)
envdoctor fix --dry-run
envdoctor fix| Detector | Severity | What it catches |
|---|---|---|
| missing | error | Variables referenced in Docker Compose (definitions + ${VAR} interpolation), Kubernetes, GitHub Actions, or .env.example but not defined in any .env file |
| undefined-in-source | error |
process.env.X / import.meta.env.X in source code with no definition in any .env file and not in .env.example
|
| unused | warning | Variables defined in .env files but never referenced in source, compose, k8s, or actions |
| duplicates | error/warning | Same key defined twice in one file (error); same key across files sharing one environment label (warning) |
| environment-diff | warning | Set-membership diffs across environments (e.g. dev vs prod) |
| type-mismatch | error | Incompatible inferred types across environments, or values failing their own inferred type |
| schema-validation | error | A value does not match its declared schema rule in the config |
| public-prefix | error | Secret-looking variable uses a public framework prefix (NEXT_PUBLIC_*, VITE_*, etc.) and would be exposed to client bundles |
| weak-secret | warning | Secret-like variable has a placeholder or very short value |
| typo | warning | A referenced name closely matches a defined name and may be a typo |
Any detector can be downgraded or disabled via the rules
config or an inline ignore.
Bootstraps a project:
- Creates
envdoctor.config.tswith commented defaults (if missing) - Generates
.env.examplefrom discovered variables (if missing) - Generates
ENVIRONMENT.mddocumentation (if missing)
Never overwrites existing files without --force.
Runs the full audit.
| Option | Description |
|---|---|
-d, --dir <path> |
Project root (default: cwd) |
--strict |
Treat warnings as errors (exit 1) |
--format <format> |
Output format: human (default), json, or sarif
|
--json |
Alias for --format json
|
--verbose |
Show file:line locations |
--only <ruleId> |
Run only specific detector(s), comma-separated |
--baseline <path> |
Suppress findings listed in a baseline file |
--write-baseline <path> |
Write current findings to a baseline file |
--staged |
Only scan files staged for commit |
--since <ref> |
Only scan files changed since a git ref (e.g. HEAD~1) |
Exit codes: 0 = clean, 1 = errors found, 2 = usage/config error
The --baseline / --write-baseline pair lets you adopt envdoctor on a legacy
project: snapshot today's findings, then fail CI only on new ones.
--staged and --since are useful in pre-commit hooks and CI to audit only the
files touched by a changeset instead of the whole repository.
Generates/updates safe artifacts based on the audit:
-
.env.exampleโ all known variables with placeholders (secrets get empty values) -
ENVIRONMENT.mdโ reference table + per-environment sections -
.github/ENVIRONMENT.mdโ checklist ofsecrets.*/vars.*for GitHub Actions (if applicable) -
env.d.tsโ TypeScript ambient declaration forprocess.envvariables -
envdoctor.schema.tsโ inferred Zod-style validation schema from observed values (e.g. integer ranges, enum sets). Import and merge it intoenvdoctor.config.tsto enable theschema-validationdetector.
| Option | Description |
|---|---|
--dry-run |
Preview changes without writing |
--force |
Overwrite without confirmation |
Focused comparison between two environments (e.g. dev prod, development production).
Shows per-variable status: โ same, โ different, โ missing.
Copy missing variable keys from one environment file to another without
overwriting existing values. Useful for keeping .env.local or .env.production
up to date after adding variables to .env.
# Append keys that exist in .env but are missing from .env.local
envdoctor sync development local
# Or by explicit file suffix
envdoctor sync .env .env.productionOnly keys are copied; values are left untouched so target-specific values and secrets stay safe.
Configuration is optional โ defaults are sensible for most projects. Create
envdoctor.config.ts (or .js/.mjs/.cjs, or an envdoctor key in
package.json):
export default {
// Glob patterns for dotenv files
envFilePatterns: [".env", ".env.*"],
// Docker Compose file patterns
composeFilePatterns: ["**/docker-compose*.y*ml", "**/compose*.y*ml"],
// GitHub Actions workflow patterns
actionsFilePatterns: [".github/workflows/**/*.y*ml"],
// Kubernetes manifest patterns
k8sFilePatterns: ["**/k8s/**/*.y*ml", "**/manifests/**/*.y*ml"],
// Source file extensions to scan
sourceExtensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs"],
// Variable names to ignore entirely (glob patterns, e.g. "AWS_*")
ignoreVariables: [],
// File paths to ignore (glob patterns)
ignoreFiles: [],
// Explicit environment label โ file list overrides
environments: {
// development: [".env", ".env.local"],
// production: [".env.production"],
},
// Fail the audit when only warnings are present
strict: false,
// Per-detector severity overrides: "error", "warning", or "off"
rules: {
// unused: "off",
// "environment-diff": "error",
},
// Per-variable value validation (feeds the schema-validation detector)
schema: {
// PORT: { type: "integer", min: 1024 },
// RATE: { type: "float", min: 0, max: 1 },
// NODE_ENV: { enum: ["development", "production", "test"] },
// API_URL: { type: "url" },
// FEATURE_FLAGS: { type: "json" },
// Optional variables are allowed to be empty/missing
// LOG_LEVEL: { type: "string", optional: true },
},
};Suppress a detector for a specific variable with a comment on the preceding line:
# envdoctor:ignore unused
DEBUG_MODE=true
# envdoctor:ignore unused, weak-secret
MY_TOKEN=placeholder| File | Label |
|---|---|
.env |
development (base) |
.env.local |
local |
.env.production |
production |
.env.<suffix> |
<suffix> |
.env.example |
example (documentation only) |
Aliases: dev โ development, prod โ production for the diff command.
Colorized, sectioned report as shown above.
{
"exitCode": 1,
"summary": {
"filesScanned": 8,
"variablesFound": 15,
"errors": 4,
"warnings": 16,
"infos": 0,
"total": 20
},
"findings": [
{
"id": "missing.COMPOSE_ONLY",
"ruleId": "missing",
"severity": "error",
"variable": "COMPOSE_ONLY",
"message": "referenced but not defined in any environment file",
"locations": [
{ "file": "docker-compose.yml", "line": 9, "kind": "definition" }
]
}
]
}Emits SARIF 2.1.0 for upload to GitHub code scanning or any SARIF-aware tool.
# .github/workflows/env-audit.yml
name: Environment Audit
on: [push, pull_request]
jobs:
envdoctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '22'
- run: npx @arunskg/envdoctor scan --strictFor code scanning, add --format sarif and upload the result with
github/codeql-action/upload-sarif.
-
Values are never printed to stdout/stderr (even with
--verbose). -
Secrets are never written to generated files (
.env.example,ENVIRONMENT.md,.github/ENVIRONMENT.md,env.d.ts). - No network calls, no telemetry โ everything runs locally.
- Secret heuristic: name matches
/(SECRET|TOKEN|PASSWORD|PASS|API[_A-Z]*KEY|PRIVATE[_-]?KEY|CREDENTIALS)/i. - Unreadable directories are skipped rather than aborting the scan.
discovery (fast-glob)
โ
โผ
parsers (dotenv, docker-compose, kubernetes, github-actions, source)
โ
โผ
normalized ProjectModel (definitions + usages per file)
โ
โผ
index (buildIndex: maps by name + environment)
โ
โผ
detectors (missing, undefined-in-source, unused, duplicates,
environment-diff, type-mismatch, schema-validation,
public-prefix, weak-secret, typo)
โ
โผ
AuditResult (Findings + Summary + ExitCode)
โ
โผ
generators (env-example, environment-doc, env-types, schema, github-actions)
Every parser implements a common Parser interface and every detector a common
Detector interface โ new formats and rules can be added without touching the
others.
npm install # install deps
npm test # run the test suite (vitest)
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run build # tsup โ dist/
# Local smoke test
node dist/index.js scan --dir tests/fixtures/sample-projectIssues and pull requests are welcome. See CONTRIBUTING.md
for the development workflow โ please run npm test, npm run lint, and
npm run typecheck before opening a PR. See CHANGELOG.md for
release history and SECURITY.md to report a vulnerability.