A dotenv alternative: drop the .env file
By Maria Otworowska,
Environment variables were never the problem
The Twelve-Factor App asked apps to "store config in the environment", and that advice still holds: config stays out of your code, and each environment gets its own values. A .env file was a shortcut for setting those variables during local development, and dotenv made the shortcut a default.
The shortcut puts real keys in a plain-text file next to your code, and three things have made that worse:
- AI coding agents read project files. Claude Code, Codex and Gemini CLI read files in your project without asking, and Gemini CLI loads .env into its own environment at startup. See Can Claude Code read your .env file?
- Copies multiply. A .env ends up in zipped folders, Time Machine, screenshots, pastebins and the next project you start by copying the last one.
- Ignoring isn't the same as protecting. .gitignore keeps the file out of git until someone runs git add -f or the ignore line gets lost in a refactor. dotenv's own README answers "Should I commit my .env file?" with "No."
When a .env file is still fine
Not every variable is a secret. A port, a log level, a feature flag or a local database URL with a throwaway password can live in a .env or in your code. Test keys that can't move money or delete data are low risk too. Move the real keys first: payment, cloud, database and AI provider keys.
The alternatives
| Approach | Example | Keys on disk in the project | Good for |
|---|---|---|---|
| Encrypted .env | dotenvx run -- npm start | Encrypted, private key elsewhere | Keeping the file in git |
| Password manager CLI | op run --env-file=.env.op -- npm start | References only | 1Password users |
| Cloud secrets manager | doppler run -- npm start | None | Teams, CI, production |
| Keychain runner | accio myapp npm start | None | Solo developers on Macs |
All four end the same way: the variables reach your app's process when it starts. They differ in where the values live, who else can reach them, and whether a key your app prints reaches an agent: op run masks output by default, dotenvx run does with --redact, and accio does when Claude Code or Codex runs it. We compare them in more detail in the best secrets managers for Mac developers and fidelius vs 1Password CLI.
Moving off .env
Here's the move with fidelius, our Mac app that keeps keys by project in your iCloud Keychain. The steps look much the same with the other tools.
1. Import the file
$ cd ~/code/myapp$ accio import
fidelius opens an import preview that lists what it will add and what it will overwrite. Nothing is saved until you click Import, and an overwritten value goes to Recently Deleted.
2. Run your commands through accio
$ accio myapp npm run dev$ accio myapp npm test
Or add local scripts next to your existing ones. Keep the plain dev and test scripts for CI and teammates, since accio only runs on Macs with fidelius:
"scripts": {
"dev": "next dev",
"dev:local": "accio myapp next dev",
"test": "vitest",
"test:local": "accio myapp vitest"
}3. Leave a list of names for others
$ accio export --exampleThis writes a .env.example with the project's own variable names and no values, so a teammate, a README or an agent knows what the project needs. Keys inherited from shared aren't listed, so add any the project depends on.
4. Delete the .env
Once the app runs through accio, delete the old file. It still holds the values in plain text. If a key in it was ever committed, pasted somewhere you don't control or seen by a coding agent, rotate it.
What you can leave in your code
In Node, require("dotenv").config() can stay: with no .env in the project, it loads nothing, and the variables accio set are already in the environment. Python's load_dotenv() is different. Without a path it searches parent folders, so it can pick up another .env higher up. Remove it, or give it an explicit path.