Stop Managing .env Files by Hand: Per-Project Env with direnv
Set up direnv so environment variables and virtualenvs load when you cd in and vanish when you leave.
What you'll build / learn
You'll set up direnv so that the moment you cd into a project, its environment variables and a project-local Python virtualenv load automatically — and unload the instant you leave. No more source .env, no more forgetting which shell has which credentials.
Prerequisites
- macOS or Linux with bash, zsh, or fish. Verified against direnv 2.37.1 on macOS (zsh) with Python 3.13.
- Homebrew on macOS, or
apton Debian/Ubuntu. - Python 3 on your PATH (only for the virtualenv step).
1. Install direnv
macOS:
brew install direnv
Debian/Ubuntu:
sudo apt install direnv
Anything else (installs the latest release binary):
curl -sfL https://direnv.net/install.sh | bash
Confirm it landed:
direnv version
2.37.1
2. Hook it into your shell
direnv works by running before every prompt, so it needs a shell hook. Add one line at the end of your shell config — it must run after anything else that manipulates your prompt (oh-my-zsh, git-prompt, rvm):
# ~/.zshrc
eval "$(direnv hook zsh)"
For bash, put eval "$(direnv hook bash)" in ~/.bashrc; for fish, put direnv hook fish | source in ~/.config/fish/config.fish. Then restart your shell:
exec $SHELL
3. Create your first .envrc
direnv looks for a .envrc file in the current directory (and its parents). It's plain bash:
mkdir myapp && cd myapp
echo 'export API_URL=https://api.example.com' > .envrc
You'll immediately see:
direnv: error /Users/you/myapp/.envrc is blocked. Run `direnv allow` to approve its content
That's the security model, not a bug — direnv refuses to execute any .envrc you haven't explicitly approved, so a cloned repo can't silently run code on cd. Approve it:
direnv allow
direnv: loading ~/myapp/.envrc
direnv: export +API_URL
Every time you edit .envrc it gets blocked again until you re-run direnv allow. Shortcut: direnv edit . opens it in $EDITOR and re-allows on save.
4. Load your existing .env file
Keep your existing .env (your framework and Docker Compose still read it) and have direnv load it too. Replace .envrc with:
# .envrc
export API_URL=https://api.example.com
dotenv_if_exists
dotenv_if_exists is from direnv's stdlib — it parses .env into the environment and doesn't complain when the file is missing (new clones won't have it). Test it:
echo 'DATABASE_URL=postgres://localhost:5432/myapp_dev' > .env
direnv allow
Commit .envrc, keep .env git-ignored — the split is exactly shared config vs. secrets.
5. Auto-activate a per-project Python
.envrc can do more than variables. Add:
layout python3
Run direnv allow, and direnv creates a virtualenv under .direnv/python-3.13/ and puts it on your PATH — activated on entry, deactivated on exit, no source .venv/bin/activate ever again. (Node users: use node reads .nvmrc, but needs $NODE_VERSIONS pointing at installed versions.)
Verify it works
Walk in and out of the project:
cd ~/myapp
direnv: loading ~/myapp/.envrc
direnv: export +API_URL +DATABASE_URL +VIRTUAL_ENV ~PATH
echo $DATABASE_URL && which python
postgres://localhost:5432/myapp_dev
/Users/you/myapp/.direnv/python-3.13/bin/python
cd ~
direnv: unloading
echo $DATABASE_URL now prints nothing — that unload on exit is the whole point.
Troubleshooting
direnv: error /path/.envrc is blocked. Run direnv allow to approve its content — Normal after creating or editing .envrc. Run direnv allow in that directory. If it's constant, use direnv edit . instead of your editor.
Nothing prints when you cd in — The hook isn't running. Check that the eval "$(direnv hook zsh)" line is the last line of ~/.zshrc (prompt frameworks loaded after it will clobber it), then exec $SHELL. direnv status reporting No .envrc or .env loaded while you're sitting next to a .envrc confirms the hook is the problem.
direnv: error .envrc file not found when running direnv allow — You're in a directory without a .envrc, or you only have a .env. direnv doesn't load bare .env files by default; add a .envrc containing dotenv_if_exists.
A variable from .envrc is empty in your shell — You wrote FOO=bar instead of export FOO=bar. Plain assignments are local to the .envrc script; only exported variables reach your environment. (.env files don't need export — the dotenv parser exports everything.)
Next steps
Skim direnv stdlib (or the man page): PATH_add bin to expose project scripts, source_env to share config across repos, watch_file to reload when other files change, plus layouts for Ruby, Go, and more. If you use a version manager like mise or nix, both integrate with direnv so .envrc also pins your entire toolchain per project.
Sources & further reading
- direnv - unclutter your .profile — direnv.net
- Setup the hook — direnv.net
- Installation — direnv.net
- direnv-stdlib man page — direnv.net
- direnv releases — github.com
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 0
No comments yet
Be the first to weigh in.