A dotenv alternative: drop the .env file

Keep the environment variables and drop the file. Your code can go on reading process.env or os.environ. Only the source of the values changes: instead of a plain-text file in the project, a command sets them when your app starts.

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

ApproachExampleKeys on disk in the projectGood for
Encrypted .envdotenvx run -- npm startEncrypted, private key elsewhereKeeping the file in git
Password manager CLIop run --env-file=.env.op -- npm startReferences only1Password users
Cloud secrets managerdoppler run -- npm startNoneTeams, CI, production
Keychain runneraccio myapp npm startNoneSolo 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 --example

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