Unclutter your shell profile with direnv
Why I load environment variables per directory with direnv instead of piling them into .zshrc.
direnv is an extension for your shell. It augments existing shells with a new feature that can load and unload environment variables depending on the current directory.
This is another utility I have been using for years, on both macOS and Linux. The idea is simple: instead of dropping every environment variable into your .zshrc (or .zprofile / .profile), you define a user-level .envrc and/or a per-project one. As you move between directories, direnv loads and unloads the right context for you.
I combine direnv with the macOS security command to load secrets for particular development workflows and projects, which is a topic for another post. Most of all, I love the finer control it gives me over what is loaded per project and per location.
Why direnv
A shell profile that only ever grows is a shell profile nobody understands. Every new project adds a variable, and after a year you have a list that no longer tells you which entry belongs to what. direnv moves those variables next to the thing that needs them.
It also adds a useful safety net. If a malicious, non-interactive script tampers with an .envrc, the change is not loaded until you explicitly run direnv allow. That deliberate pause is exactly what you want for a file that can run arbitrary code on cd.
It is worth knowing that direnv does ship a default allow-list of trusted patterns, such as layout and source, which are evaluated without a prompt. Those are the exceptions, not the rule, and the direnv security documentation spells out exactly what is trusted and why.
Install
On macOS, Homebrew handles it:
brew install direnv
Then add the shell hook to the end of your .zshrc so it runs on every prompt:
eval "$(direnv hook zsh)"
A quick demo
# Create a test folder.
$ mkdir ~/test-project1
$ cd ~/test-project1
# Show that MY_VAR is not loaded.
$ echo ${MY_VAR-notset}
notset
# Create a new .envrc file with an export. .envrc accepts Bash code.
$ echo export MY_VAR=hello > .envrc
direnv: error /Users/rui/test-project1/.envrc is blocked. Run `direnv allow` to approve its content
# We are required to allow the configuration change to be loaded.
$ direnv allow
direnv: loading ~/test-project1/.envrc
direnv: export +MY_VAR
# Now we can verify that the value is set.
$ echo ${MY_VAR-notset}
hello
# Exiting the project unloads the configuration.
$ cd ..
direnv: unloading
# Show that MY_VAR is not loaded.
$ echo ${MY_VAR-notset}
notset
Conclusion
direnv is the kind of utility I include in every developer setup, and a must-have for my macOS setup. I love the control it gives me, and the fact that I can combine project-level and user-level configuration with simple auto-load and auto-unload mechanics.
If you are working through the first things I install on a new macOS setup, this is a natural next step: once the shell is predictable, give it a way to change shape as you move around.
Comments
Comments are powered by GitHub Discussions. Sign in with a GitHub account to join the conversation.