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.

Related

Comments

Comments are powered by GitHub Discussions. Sign in with a GitHub account to join the conversation.