Tools

direnv: load per-project environment variables automatically when you cd

TL;DR

direnv loads variables from .envrc when you enter a directory and unloads them when you leave, giving each project its own environment. Hook it into your shell, re-run direnv allow after changes, and keep secrets in ignored files.

Each project needs its own environment variables: DATABASE_URL, AWS_PROFILE, a port, a path to tools in ./bin. Either you cram them all into ~/.zshrc (mixed between projects, easy to hit the wrong environment), or you have to remember source .env every time you open a terminal. direnv handles it cleanly: when you cd into a directory with an .envrc file, variables load automatically; when you leave, they're unloaded. Each project gets its own environment without interference.

GitHub page of direnv/direnv
GitHub page of direnv/direnv (screenshot, November 2026)

GitHub: https://github.com/direnv/direnv · about 16k stars · Go · MIT

Install and hook into your shell

brew install direnv          # macOS
sudo apt install direnv      # Ubuntu/Debian

# Add to the END of ~/.zshrc
eval "$(direnv hook zsh)"
# bash: eval "$(direnv hook bash)"   fish: direnv hook fish | source

The hook is mandatory; without it direnv does nothing.

Try it

cd ~/work/my-api
echo 'export DATABASE_URL=postgres://localhost/my_api_dev' > .envrc
# direnv: error .envrc is blocked. Run `direnv allow` to approve its content
direnv allow
# direnv: loading .envrc
# direnv: export +DATABASE_URL

echo $DATABASE_URL     # set
cd ..
# direnv: unloading
echo $DATABASE_URL     # empty

direnv allow is a safety layer: .envrc is shell code, so direnv only runs files you've approved. Whenever the file changes (say, after git pull), you must allow it again.

Helper functions in .envrc

FunctionEffect
dotenvLoad variables from the directory's .env file
PATH_add binPrepend ./bin to PATH (as an absolute path)
layout python3Create and activate a Python virtualenv for the project
source_upAlso load the parent directory's .envrc (for monorepos)
use nixLoad an environment from Nix, for Nix users

A realistic .envrc

# .envrc (commit this file)
export AWS_PROFILE=myapp-dev
export PORT=3000
PATH_add node_modules/.bin
PATH_add scripts
dotenv_if_exists .env.local    # secrets live in .env.local, NOT committed
# .gitignore
.env.local
.direnv/

Common commands

  • direnv allow / direnv deny: approve / block the current .envrc.
  • direnv reload: reload after editing without leaving and re-entering.
  • direnv status: show status and which file is loaded.
  • direnv edit .: open .envrc in your editor and allow it on save.

Caveats

  • Don't commit secrets: move sensitive values to an ignored file and load it with dotenv_if_exists.
  • Read an unfamiliar repo's .envrc before running allow, because it's code that runs on your machine.
  • Variables only exist in the shell; apps launched from the Dock or an IDE that doesn't go through the shell won't see them unless the IDE has a direnv plugin.
  • If you already use mise, its [env] section does something similar; pick one tool to avoid overlap.

How to apply it

  • Dev: Move each project's variables out of .zshrc into its own .envrc; no more hitting another project's database or AWS profile.
  • Tech Lead: Commit an .envrc without secrets plus a .env.example; new developers copy the example and run direnv allow.
  • PM: Fewer "command run against the wrong environment" incidents, a class of error that can cause data loss.

Sources & references

  1. direnv/direnv — direnv (GitHub)
  2. direnv documentation — direnv