missing-env
CLI, das jede Umgebungsvariable findet, die der Code liest und niemand gesetzt hat. Auf npm, ohne eine einzige Laufzeit-Abhängigkeit.
missing-env5 Dateien in 15 ms
2 missing — vom Code gelesen, nirgends gesetzt
1 empty — definiert, aber ohne Wert
4 undocumented — ein frischer Klon wüsste nichts davon
2 unset — der Code fällt auf einen Standardwert zurück
1 unused — definiert, aber nie gelesen
2 Fehler, 3 Warnungen, 5 Hinweise
- Laufzeit-Abhängigkeiten
- 0
- Tests
- 69
- Sprachfamilien
- 11
Über das Projekt
Jemand fügt process.env.STRIPE_SECRET_KEY hinzu, setzt den Wert auf seinem Rechner und vergisst, ihn aufzuschreiben. Zwei Wochen später klont ein neuer Kollege das Repository, die Anwendung startet ohne Fehler, und der Wert taucht als undefined mitten in einem Zahlungsvorgang wieder auf. Das ist kein exotischer Fall, das passiert in jedem Team.
Es gibt Werkzeuge, die .env-Dateien prüfen. Die haben alle dasselbe Problem: Sie kennen nur die Datei, nicht das Programm. missing-env geht den umgekehrten Weg. Es liest den Quellcode, sammelt jeden Lesezugriff auf eine Umgebungsvariable und vergleicht diese Liste gegen .env und .env.example. Fehlt eine Pflichtvariable, ist der Exit-Code 1 — damit taugt der Aufruf als Gate in der CI, nicht nur als Bericht für Menschen.
Der Grund, warum das kein grep ist, steht in einer einzigen Datei: dem Lexer. Ein auskommentierter Zugriff ist keiner, ein Vorkommen in einem String-Literal ist keiner, der Code in einem Template-Loch dagegen schon. Kommentare werden dabei mit Leerzeichen überschrieben statt gelöscht, damit Byte-Offsets und Zeilennummern exakt bleiben — eine Fundstelle, die auf die falsche Zeile zeigt, ist schlimmer als keine. Dieselbe Schicht kennt Python-Docstrings, verschachtelte Rust-Blockkommentare und den Unterschied zwischen '…' und "…" in Shell-Skripten, wo in doppelten Anführungszeichen ${VAR} weiterhin Code ist.
Die zentrale Entscheidung ist, was überhaupt Pflicht ist. Dafür gibt es genau eine Regel: Ein Lesezugriff ist optional, wenn die Aufrufstelle einen Ersatzwert liefert. process.env.DATABASE_URL ist Pflicht, process.env.LOG_LEVEL ?? 'info' nicht. Zwei Ausnahmen gelten auch ohne Default als optional, weil die API den Aufrufer zwingt, das Fehlen zu behandeln: Gos os.LookupEnv und Rusts option_env!. Diese Einheitlichkeit war Absicht — eine Regel, die sich in einer Zeile README erklären lässt, wird auch geglaubt.
Erkannt wird das in elf Sprachfamilien: JavaScript samt TypeScript, Python, Go, Ruby, PHP, Java samt Kotlin und Scala, C#, Rust, Shell-Skripte, YAML inklusive docker-compose und Dockerfiles. Das Ganze sind 1.774 Zeilen Quellcode gegen 748 Zeilen Tests, null Laufzeit-Abhängigkeiten und als Entwicklungsabhängigkeiten nur TypeScript, Prettier und die Node-Typen. Das Paket liegt unter MIT auf npm, eine GitHub Action verpackt denselben Aufruf für Pipelines, und die CI prüft Node 20, 22 und 24.
Den peinlichsten Fehler fand das Werkzeug an sich selbst: Eigene ignore-Angaben ersetzten die Standard-Ausschlüsse, statt sie zu ergänzen — womit plötzlich node_modules mitgescannt wurde. Ein Linter, den man auf sein eigenes Repository loslässt, ist die ehrlichste Testmethode, die es für so ein Werkzeug gibt.
Beispiele
Warum kein grep
// process.env.OLD_KEY ← a commentconst hint = 'set process.env.API_KEY' // ← inside a stringconst url = `${process.env.API_URL}/v1` // ← the only real readEin grep findet alle drei Zeilen, ein echter Lesezugriff ist nur die letzte. Deshalb läuft jede Datei zuerst durch einen Lexer: Kommentare fallen weg, Treffer in Strings zählen nicht, der Code in einem Template-Loch dagegen schon.
Pflicht oder optional
process.env.DATABASE_URL // requiredprocess.env.LOG_LEVEL ?? 'info' // optionalconst { PORT = 3000 } = process.env // optionalEine Regel für alle Sprachen: Ein Lesezugriff ist optional, wenn die Aufrufstelle einen Ersatzwert liefert. Alles ohne Rückfallwert ist Pflicht — und genau das lässt den Build scheitern.
Dieselbe Regel, anderes Ökosystem
os.environ["SMTP_HOST"] # requiredos.getenv("SMTP_PORT", "587") # optionalZwei APIs gelten auch ohne Default als optional, weil sie den Aufrufer zwingen, das Fehlen zu behandeln: Gos os.LookupEnv und Rusts option_env!.
Als Gate in der CI
- uses: actions/checkout@v5- uses: Jxxck/missing-env-action@v1Zwei Zeilen. Der Schritt schlägt fehl, sobald eine Pflichtvariable nirgends gesetzt ist, und schreibt die Fundstellen als Tabelle in die Job-Zusammenfassung. Ohne GitHub tut `npx missing-env --quiet` dasselbe.
Die Beispieldatei ehrlich halten
$ npx missing-env --sync-example wrote 4 variables to .env.example # src/billing.ts:3STRIPE_SECRET_KEY=# worker/reports.py:4BATCH_SIZE=Ergänzt jede undokumentierte Variable samt Fundstelle. Werte werden nie aus der .env übernommen — eine Beispieldatei landet im Git, und genau so kommen Zugangsdaten in die Versionsgeschichte.
Als Bibliothek
import { check } from 'missing-env' const report = await check({ root: process.cwd() })const blocking = report.findings.filter((f) => f.severity === 'error')Dieselbe Prüfung als Funktion, wenn die Ausgabe woanders hinsoll. Wer die Bausteine einzeln braucht, bekommt scanProject, parseEnv, diagnose und render auch getrennt. `--json` gibt denselben Bericht auf der Kommandozeile aus.