TL;DR
- dotenv — loads
.envinto your app's process at runtime. Lives inside the app. - direnv — loads
.envrcinto your shell when youcdinto the directory. Lives outside the app. - They solve different problems. Many teams use both.
What dotenv actually does
dotenv is a library you import in your app code. On startup, it reads a .env file and merges the values into process.env. That's it. The shell you launched the app from doesn't see these variables at all.
// server.js
import 'dotenv/config';
console.log(process.env.DATABASE_URL); // works
// terminal
$ echo $DATABASE_URL
# empty — dotenv only loaded it into the Node processWhat direnv actually does
direnv is a shell extension. When you cd into a directory with an .envrc, it automatically exports those variables into your current shell. When you cd out, they unload.
$ cd ~/projects/api
direnv: loading ~/projects/api/.envrc
direnv: export +DATABASE_URL +JWT_SECRET
$ echo $DATABASE_URL
postgres://localhost/api
$ cd ..
direnv: unloadingNow every CLI tool you run — psql, redis-cli, pytest, ad-hoc scripts — sees the right values automatically. No source .env, no wrapping commands in node --env-file=.env.
When to use which
| Scenario | Pick |
|---|---|
Your app needs process.env.KEY at runtime | dotenv (or Node 20+ --env-file) |
You want psql / redis-cli to "just work" in the project dir | direnv |
| Python venv + env vars activated together | direnv (via layout python) |
| CI/CD pipelines | Neither — use host env injection |
| Production | Neither — host dashboard or secrets manager |
Using them together
The common production-grade setup on macOS/Linux dev machines:
# .envrc (loaded into shell by direnv)
dotenv # loads .env into shell via direnv's dotenv helper
source .venv/bin/activate # if Python
export PATH="./node_modules/.bin:$PATH"
# .env (consumed by your app at runtime via dotenv lib)
DATABASE_URL=postgres://localhost/app
JWT_SECRET=...Now npm run dev works (dotenv lib reads .env) and psql in your terminal hits the right DB (direnv exported DATABASE_URL to the shell).
Security considerations
Both .env and .envrc sit in your repo directory and should be git-ignored (how-to).
direnv has one safety feature dotenv doesn't: allow-listing. When you first enter a directory with an .envrc, direnv refuses to load it until you explicitly run direnv allow. This prevents a malicious git clone from exporting attacker-controlled env vars into your shell.
Switching between the two
Going from dotenv to direnv: keep .env, add a .envrc that just contains dotenv. Done.
Going from direnv to dotenv: move anything that isn't KEY=value (shell functions, PATH manipulation, venv activation) out of .envrc and into a wrapper script. What's left goes into .env.
FAQ
Can direnv load a .env file directly, or does it need its own .envrc syntax?
direnv ships a built-in dotenv function for exactly this — adding a single line, dotenv, to your .envrc loads the same .env your app already reads, so you don't maintain two copies of the same values.
Does direnv work on Windows?
Not natively — direnv is a Bash/Zsh/Fish shell hook, so on Windows it requires WSL. If your team is on native Windows without WSL, dotenv (or Node's built-in --env-file) is the more portable choice.
Is direnv's allow-list a real security boundary, or just a prompt?
It's a real one, backed by a hash of the file's contents stored outside the repo. If the .envrc changes after you've allowed it — say, a malicious edit lands via a pulled branch — direnv blocks it again until you explicitly re-run direnv allow.
Can I use direnv with Docker or a devcontainer?
direnv operates on your host shell, not inside a container, so it doesn't apply directly inside a Docker build or a running container's shell. Inside a devcontainer, install direnv as part of the container setup if you want the same behaviour there.
Recommendation
For solo projects: start with dotenv. For any team larger than one, or if you regularly run CLI tools against your dev DB, add direnv on top. The combination is the best-in-class local-dev env setup in 2026.
For production, neither of these is the answer — see our dotenv vs secrets manager comparison.