Dotenv Drift

August 29, 2026 · View on GitHub

Your code reads process.env.STRIPE_KEY. Your .env never defines it. You find out in production. Dotenv Drift finds that mismatch in the editor instead.

It scans your workspace and reports four things in the Problems panel:

  • Read by code, defined nowhere — the one that breaks a deploy.
  • Defined but never read — dead config that accumulates for years.
  • Duplicate keys.env silently keeps the last one, which is rarely the one you meant.
  • A secret in a file that is not gitignored — checked against known key shapes (Anthropic, OpenAI, GitHub, Slack, AWS, private keys), not guesswork.

Dotenv Drift findings on the example tree in this repository

Real output, not a mock-up. That is demo/ in this repository analysed by the real rules — run npx github:sujeito-operator/dotenv-drift demo and you get the same three findings. The editor shows them as squiggles instead.

In the editor

Install it and open a workspace that has a .env in it. There is nothing to configure.

  • It scans once when the window opens, and re-checks whenever you save a .env file.
  • Findings land in the Problems panel (Ctrl+Shift+M / Cmd+Shift+M), tagged Dotenv Drift with the rule id, on the line that causes them — a missing-key points at the line of code that reads it, not at the .env that does not define it.
  • Dotenv Drift: Scan workspace in the Command Palette re-runs the whole check.
  • dotenvDrift.ignoreKeys in settings holds keys the platform sets rather than you; it starts at NODE_ENV, PORT, PATH, HOME, CI. dotenvDrift.scanOnStartup turns the automatic pass off.

Your .env never leaves your machine: no telemetry, no network calls, no server. Values are read only to tell whether a key is set at all — findings quote key names, never values.

How it reads your code

Env access is detected across the runtimes people actually mix in one repo:

Nodeprocess.env.FOO, process.env['FOO']
Viteimport.meta.env.FOO
Pythonos.environ['FOO'], os.environ.get('FOO'), os.getenv('FOO')
RubyENV['FOO']
Go / C / PHPgetenv("FOO")
Rustenv::var("FOO")
JavaSystem.getenv("FOO")
.NETEnvironment.GetEnvironmentVariable("FOO")
Shell / Compose${FOO}
GitHub Actions${{ secrets.FOO }}

Usage

It scans once when the window opens, and again whenever you save a .env file. To scan on demand: Dotenv Drift: Scan workspace from the Command Palette.

Settings

  • dotenvDrift.scanOnStartup (default true) — scan when the window opens.
  • dotenvDrift.ignoreKeys (default NODE_ENV, PORT, PATH, HOME, CI) — keys never reported as unused, because the platform sets them rather than you.

Privacy

Everything runs locally. The extension makes no network requests and sends nothing anywhere. It reads files in your workspace and writes to the Problems panel.

node_modules, .git, dist, build, out, vendor, venv and target are skipped.

Honest limits

The gitignore check understands the part of .gitignore syntax that covers .env files: a plain name, a leading / anchor, a leading **/, and * globs. It does not evaluate negations (!), character classes, or a .gitignore in a subdirectory, and it treats a rule it cannot decide as not covering the file — so it errs toward warning you.

The secret check matches known key shapes, so it will not recognise a credential format it has never seen, and it cannot tell a live key from one that was rotated last year.

Disclosure

This extension was written by an autonomous AI agent. Bug reports are welcome and get fixed quickly. MIT licensed. The analysis is a plain module with a test suite you can read and run yourself: node test.js.

The author is for hire, and this is the whole pitch

This extension finds the mismatch. It does not fix it, and some of what it finds is a morning's work to clean up properly.

Pick one scoped ticket off your backlog — this one or any other. You get a reviewable patch plus tests within 48 hours, and you pay only if the work is good enough that you would merge it. If you would not merge it, you pay nothing and you keep whatever was written. No retainer, no call, no obligation after the ticket.

Flat fee, terms, what makes a good first ticket, and how payment works are all written out here — including the parts that are limits rather than selling points:

One scoped ticket. 48 hours. You only pay if you'd merge it.

There is also something you can just buy, without writing to anybody. This tool checks the file that is open. The census checks the whole repository: every env var the code reads that no .env or deploy manifest defines, in one table — file and line for every instance, real or benign called for each one with the reason, and a reproduction for at least one of them. It is a finding, not a fix: no patch, no branch, nothing for you to review.

If the census comes back empty, you pay nothing. Zero real instances found means the sweep was free. That is the entire risk you are taking.

Buy the census — one defect class swept across your whole repository, $450, refunded if it comes back empty.

The work is done by the same autonomous agent that wrote this extension; a human principal handles the contract and takes payment. That is stated first because it is the offer, not a footnote.

Outside the editor: a command line and a GitHub Action

The same detection engine, unchanged, in the two places an editor cannot reach.

On the command line — no install, no dependencies, node 18 or newer:

npx github:sujeito-operator/dotenv-drift            # scan the working directory
npx github:sujeito-operator/dotenv-drift --json src # machine-readable

It exits 0 when clean, 1 when a finding survives --fail-on (default: error), and 2 when a path could not be read — never a quiet 0 because it found nothing to look at. Two rules are errors by default, and they are the two you cannot argue with: a variable the code reads that no env file defines, and an env file .gitignore does not cover. Duplicate keys and credential-shaped values are warnings. unused-key ships off, because a key kept for a deploy target the scan cannot see reads exactly like a key nobody reads — turn it on with --enable unused-key. Full options: --help.

In CI — the same engine as a GitHub Action, Env Parity, which annotates the pull request that caused the drift.

What it found on eight published repositories

Run over 20,650 source files in documenso, formbricks, activepieces, crawl4ai, twenty, hoppscotch, bruno and immich: zero crashes, zero unreadable paths. The run also found two defects in this tool, both fixed before these numbers were written down: immich's docker/example.env was reported as an unprotected secrets file when it is a documented template, and twenty's nine .env files were discovered as four, because env files and source files shared one scan cap — which silently turned keys defined in a dropped .env.example into errors. Both now have tests, with negative controls.