· 3 minutes read direnvvaultdevex

Back to Basics: direnv

Every post is about the next AI tool. This one goes back to basics: a small, old tool that made onboarding at work a non-event.

Most of what I read these days is about something new. A new model, a new agent, a new way to not write code. Fine. This post goes back to basics: a small tool that has been around for years and quietly removed a whole category of onboarding problems at work.

The tool is direnv.

The problem

Every project needs an environment to run. Default config, like which Vault to talk to or the log level. And secrets, like a database password. Before, everyone kept a local .env with all of that copied in by hand. It worked on day one. Then a password was rotated or a default changed, and your copy was quietly out of date. And since that file was full of real values, it was always one .gitignore slip away from a commit.

What direnv does

direnv loads environment variables when you cd into a directory and unloads them when you leave. The variables come from a file called .envrc at the root of the project. In other words, the environment follows the directory you’re in.

What makes it work is the shell hook. One line in your shell config:

# ~/.zshrc
eval "$(direnv hook zsh)"

From then on, direnv runs before every prompt. Enter a directory with an .envrc and it loads it. Leave it and your environment goes back to how it was. Edit the .envrc and it reloads. You never call it yourself.

Note

direnv won’t run a file it hasn’t seen before. The first time, and after every change, it asks you to run direnv allow. An .envrc is a shell script, so that’s the right default.

Our .envrc

Every repo has one, committed next to the code. Plain exports for default config, a Vault login, and the secrets pulled from Vault:

export VAULT_ADDR=https://vault.example.com
export LOG_LEVEL=debug

vault token lookup >/dev/null 2>&1 || vault login -method=oidc >/dev/null
export DB_PASSWORD=$(vault kv get -field=password secret/myapp/dev/db)

The vault token lookup line checks whether you already have a valid token. If you don’t, vault login -method=oidc opens the browser, you sign in with the same account as everything else, and the token is cached. Most days that line does nothing.

So what changed?

  • Secrets are always in sync. They come from Vault every time you enter the directory. When a password is rotated, nobody has to tell anyone.
  • No stale local config. Defaults are reviewed and merged like any other change. You pull, direnv asks you to allow the new version, and you’re up to date.
  • Nothing secret to commit by mistake. The .envrc is meant to be committed, and it holds paths into Vault, not values.
  • The app knows nothing about Vault. It reads environment variables and that’s it. No Vault client, no credentials of its own, whatever the language.
  • Onboarding is a clone. Install direnv and the Vault CLI, add the hook, clone the repo, run direnv allow.

A word of caution

There’s one catch, and it brings us back to the intro. The secrets live in your shell, not in the app. Every process started from that terminal inherits them: a script, an npm postinstall, or an AI agent working in the repo.

Those agents are really into our systems now. They read files, run commands, and see DB_PASSWORD like everything else. In other words, your convenience is also a security risk. If your AI agent runs in a sandbox, there’s no issue. I’m sure that’s not the case for everyone yet.

It just works

Once it’s adopted, direnv becomes one of those tools that is so stable and so obvious that you forget about it. Nobody questions it anymore. You switch to a repo and you expect your context to already be loaded.