envdoctor

Reconciles ENV usage in Ruby source against .env files: reports undefined-in-source (error) and unused (warning). Local-first, no network.


Keywords
ci-cd, cli, devtools, docker-compose, dotenv, dotenv-linter, environment-variables, github-actions, golang, java, kubernetes, linter, nodejs, perl, php, polyglot, python, ruby, static-analysis, typescript
License
MIT
Install
gem install envdoctor -v 0.1.2

Documentation

@arunskg/envdoctor

npm version npm downloads total downloads CI node license PRs welcome Code of Conduct

PyPI Gem Packagist Maven Central Go module

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.

๐Ÿ“ˆ Download trends

npm daily downloads, last 90 days

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 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Contents

Why

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.

Supported formats

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)

Installation

# From npm
npm install -g @arunskg/envdoctor

# Or run directly with npx
npx @arunskg/envdoctor scan

Using another language? See native ports below.

Native ports

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 too

Note: the Python distribution is named arun-envdoctor on PyPI (the bare name is blocked as too similar to an existing project), but the installed command and importable package are both envdoctor.

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/envdoctor from your project directory, not from inside a checkout of this repo โ€” npx resolves the local package first, whose envdoctor bin isn't on your PATH, and you'll see envdoctor: command not found. After a global install, the short envdoctor command works anywhere.

Quick start

# 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

Detectors

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.

Commands

envdoctor init [--force]

Bootstraps a project:

  • Creates envdoctor.config.ts with commented defaults (if missing)
  • Generates .env.example from discovered variables (if missing)
  • Generates ENVIRONMENT.md documentation (if missing)

Never overwrites existing files without --force.

envdoctor scan [options]

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.

envdoctor fix [options]

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 of secrets.*/vars.* for GitHub Actions (if applicable)
  • env.d.ts โ€” TypeScript ambient declaration for process.env variables
  • envdoctor.schema.ts โ€” inferred Zod-style validation schema from observed values (e.g. integer ranges, enum sets). Import and merge it into envdoctor.config.ts to enable the schema-validation detector.
Option Description
--dry-run Preview changes without writing
--force Overwrite without confirmation

envdoctor diff <env1> <env2> [--json]

Focused comparison between two environments (e.g. dev prod, development production). Shows per-variable status: โœ“ same, โš  different, โŒ missing.

envdoctor sync <source> <target>

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.production

Only keys are copied; values are left untouched so target-specific values and secrets stay safe.

Configuration

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 },
  },
};

Inline ignores

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

Environment labels

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.

Output formats

Human (default)

Colorized, sectioned report as shown above.

JSON (--json / --format json)

{
  "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" }
      ]
    }
  ]
}

SARIF (--format sarif)

Emits SARIF 2.1.0 for upload to GitHub code scanning or any SARIF-aware tool.

CI integration

# .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 --strict

For code scanning, add --format sarif and upload the result with github/codeql-action/upload-sarif.

Security

  • 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.

Architecture

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.

Development

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-project

Contributing

Issues 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.

License

MIT