2026-05-08
Every developer has done this dance: export AWS_PROFILE=client-foo, work for an hour, switch to another project, forget to unset, accidentally deploy to the wrong account. Or you maintain a .env file and source it manually. Or worse — you stuff client secrets into ~/.bashrc and they leak into every subshell, every ps dump, every screen share.
direnv solves this with one trick: it hooks into your shell prompt and loads/unloads environment variables based on the directory you're in. Walk into a project, vars appear. Walk out, they vanish. No sourcing, no remembering, no leaks.
Install and hook it once:
$ apt install direnv # or brew, or pacman
$ echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
Then in any project directory, drop a .envrc file:
$ cd ~/work/client-foo
$ cat > .envrc <<'EOF'
export AWS_PROFILE=client-foo
export DATABASE_URL=postgres://localhost/foo_dev
export PATH="$PWD/bin:$PATH"
EOF
$ direnv allow
direnv: loading ~/work/client-foo/.envrc
direnv: export +AWS_PROFILE +DATABASE_URL ~PATH
The direnv allow step is the security model — direnv refuses to execute an .envrc until you explicitly trust it. Edit the file, and you must allow it again. This means a git pull can never silently inject env vars into your shell.
The killer feature is the stdlib. .envrc is just bash, but direnv ships helper functions that handle 90% of what you'd write by hand:
# Load .env file (the simple case)
dotenv
# Load only if file exists, no error otherwise
dotenv_if_exists .env.local
# Add bin/ to PATH relative to .envrc location
PATH_add bin
PATH_add node_modules/.bin
# Walk up the tree and source the first match
source_up
# Require a var to be set, fail loudly if not
strict_env
export API_KEY=${API_KEY:?must be set in ~/.envrc.private}
The source_up trick is gold for monorepos: put shared vars in the repo root .envrc, and per-service overrides in subdirectory .envrc files. Each service gets the union, automatically.
It also integrates with version managers without their shell hooks:
use python 3.11 # creates/activates a venv in .direnv/
use node 20 # via nodenv/asdf
use nix # full nix-shell environment, but only here
layout python # auto-creates virtualenv, no manual activation
That last one is why I haven't typed source venv/bin/activate in years. cd into a Python project, venv activates. cd out, it deactivates. The venv lives in .direnv/ which you gitignore.
One pattern I use constantly — splitting public config from secrets:
# .envrc (committed)
export DATABASE_HOST=db.internal
export S3_BUCKET=foo-prod
source_env_if_exists .envrc.private
# .envrc.private (gitignored)
export AWS_SECRET_ACCESS_KEY=...
export STRIPE_SECRET_KEY=...
The team shares the public file via git; secrets stay local. No .env.example drift, no Slack messages saying "what was that variable name again?"
The mainstream alternative is sourcing .env files manually or using IDE-specific env loading. Both pollute the parent shell or only work inside one tool. direnv works for every command you run from that directory — make, psql, ad-hoc scripts, your editor's terminal — without any of them needing to know direnv exists.
source .env rituals with directory-scoped environments that load on cd in and unload on cd out — secure by default, transparent to every tool you run.
