Skip to content

direnv: Scoping Secrets for Polyglot, Poly-Project Development

Stop putting secrets in your shell profile. If you work across multiple cloud projects, languages, and infrastructure stacks concurrently, direnv scopes your environment per directory—so the right credentials are active for the right project, and nothing leaks sideways.

This is a short post about a small tool that eliminates an entire class of “wrong account” mistakes.

If you work on one project with one cloud account, environment variables in .zshrc are fine. Export your tokens, source your profile, move on.

That stops working when you’re a polyglot developer juggling multiple projects with different cloud providers, different accounts, different API keys, and different infrastructure stacks. The .zshrc approach gives you a flat namespace:

Terminal window
# .zshrc — which project are these for?
export GITHUB_TOKEN="ghp_..."
export DATABASE_URL="postgres://..."
export SUPABASE_ANON_KEY="eyJ..."
export TF_VAR_project="my-gcp-project"
export NEON_API_KEY="..."
export PORKBUN_API_KEY="..."

Every shell session sees every secret. Switch from Project A to Project B? Same DATABASE_URL. Same TF_VAR_project. Same everything. You’re one terraform apply away from modifying the wrong infrastructure because you forgot which project’s credentials are loaded.

This isn’t hypothetical. If you’ve ever run a command against the wrong cloud project, you know the feeling.

direnv loads and unloads environment variables based on your current directory. You put an .envrc file in a project directory, and when you cd into it, those variables are set. When you cd out, they’re unset.

~/workspace/travel/infra/.envrc
export TF_VAR_project="travel-prod"
export TF_VAR_db_password="$(bw get password 'travel-db')"
export SUPABASE_ANON_KEY="eyJ..."
# ~/workspace/angzarr/core/.envrc
export DATABASE_URL="postgres://localhost:5432/angzarr_dev"
export RUST_LOG="info"

cd ~/workspace/travel/infra — travel credentials loaded. cd ~/workspace/angzarr/core — travel credentials gone, Angzarr credentials loaded. No manual sourcing, no remembering which project you’re in, no stale variables from the last directory.

Install direnv, hook it into your shell, and trust your .envrc files:

Terminal window
# Install (Debian/Ubuntu)
sudo apt install direnv
# Hook into zsh (~/.zshrc)
eval "$(direnv hook zsh)"
# Create a project .envrc
echo 'export MY_VAR="value"' > ~/workspace/my-project/.envrc
# Trust it (required on first use or after changes)
direnv allow ~/workspace/my-project

That’s it. There’s no daemon, no config file, no service to manage.

The value scales with the number of projects you maintain. With two projects, you might remember which credentials are active. With five—each with its own cloud provider, database, API keys, and Terraform state—you won’t.

direnv gives you:

Isolation by default. Project A’s credentials don’t exist in Project B’s shell. You can’t accidentally terraform apply against the wrong account because the wrong account’s variables aren’t set.

Secret scoping. API keys, database URLs, and cloud credentials live next to the project that uses them, not in a global profile that every project inherits. Add .envrc to .gitignore and secrets stay local.

Composability. .envrc files can source other files, call CLI tools (like bw for Bitwarden or gcloud for GCP tokens), and inherit from parent directories. A team-level .envrc can set shared defaults while a project-level one overrides specifics.

Onboarding simplification. “Clone the repo, create an .envrc with these variables, run direnv allow” is easier to communicate and harder to get wrong than “add these twelve exports to your shell profile and make sure they don’t conflict with your other projects.”

Audit your shell profile. Anything project-specific should move to an .envrc:

Keep in .zshrcMove to .envrc
PATH modificationsDATABASE_URL
Shell aliasesTF_VAR_*
Tool initialization (nvm, pyenv)Cloud API keys
Editor configProject-specific tokens
General preferencesSUPABASE_*, NEON_*, etc.

The rule: if it’s specific to a project or cloud account, it belongs in that project’s .envrc. If it’s about your development environment in general, it stays in .zshrc.

Your .envrc files contain secrets. They should not be committed:

.gitignore
.envrc

If you want to share the structure without the values, commit an .envrc.example:

Terminal window
# .envrc.example — copy to .envrc and fill in values
export DATABASE_URL=""
export TF_VAR_project=""
export API_KEY=""

The Small Tool That Prevents the Big Mistake

Section titled “The Small Tool That Prevents the Big Mistake”

direnv is a small tool. It does one thing. But for developers working across multiple cloud projects, languages, and infrastructure stacks, that one thing eliminates an entire category of mistakes that range from embarrassing to catastrophic.

The cost of adopting it is one line in your .zshrc and a few minutes moving exports into .envrc files. The cost of not adopting it is eventually running terraform destroy against production because you forgot you were still pointing at the wrong project.


This is one of those tools where the setup time is measured in minutes and the first “oh, that would have been bad” moment comes within the week.