12 December 2021

Tools You Should Know About: direnv

In a nutshell

From the homepage:

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.

There's really not much more to it: it's a very simple feature, but I've found it tremendously useful and so I wanted to spread the word. At this point, setting up direnv is pretty much the first thing I do when working on a new project, and I wouldn't want to work without it.

Why you should know about it

In my experience, managing poject-dependent configuration has always been a bit of a pain. That is, until I discovered direnv a couple years ago.

Environment variables are definitely not the only way to manage configuration (it's just the best one), but there are many tools that rely on them, and many more that offer them as an option. Maybe you need to set a library path, or a locale, or a project-global version for some dependency in a multi-language project. Maybe you just want all your scripts to know the path to the project root. Maybe you want to set your Docker daemon coordinates. Maybe you have a bin folder in the some project and you'd like to have that in PATH, but only when you're working on that project.

Whenever a problem could be solved by setting an environment variable, direnv can help. More importantly, if you need to set different sets of variables across different projects, it can quickly become error-prone. direnv solves that.

Feature highlights

direnv is a very simple tool, but it still packs a punch.

The .envrc file

The core of what direnv does is based on the envrc file. direnv extends your shell to check the current path (and its ancestors) for a file called .envrc, and if there is one, it loads it (see below if you're concerned about the security aspect of that).

That file is expected to mostly set environment variables, and direnv will unset them when you cd out of that directory (and its children).

$ cat myproject/.envrc
export MY_VAR=in-project
$ export MY_VAR=outside
$ echo $MY_VAR
outside
$ cd myproject
direnv: loading /myproject/.envrc
direnv: export ~MY_VAR
$ echo $MY_VAR
in-project
$ cd ..
direnv: unloading
$ echo $MY_VAR
outside
$ cd myproject
direnv: loading /tmp/myproject/.envrc
direnv: export ~MY_VAR
$ export MY_VAR=1
$ echo $MY_VAR
1
$ cd ..
direnv: unloading
$ echo $MY_VAR
outside
$

Security

direnv will not just randomly execute any code anywhere: it works with an allowlist system. Whenever it encounters a .envrc file, it only runs it if this exact content for this exact path has been authorized. If not, it will print out a warning to signal that there is an .envrc file but it has not been loaded. Adding the file to the allowlist can be done by running direnv allow, which will also immediately load it.

$ cd myproject
direnv: error /tmp/myproject/.envrc is blocked. Run `direnv allow` to approve its content
$ direnv allow
direnv: loading /tmp/myproject/.envrc
direnv: export ~MY_VAR
$ echo $MY_VAR
in-project
$

Obviously in general one should take a look at the file before allowing it.

Private values

A common idiom is to end the .envrc file with a line that looks like:

source_env_if_exists .envrc.private

What this means is: if the .envrc.private file exists, load it. As the name suggests, the .envrc.private file is generally meant for values that should not be shared, and that file should therefore not be committed.

This can range from credentials to IDE configuration and the like. And the variables set in .envrc.private will get loaded and unloaded in the same way as the rest of .envrc, meaning this makes it super easy to manage different sets of credentials across different projects.

Conclusion

That's about it. A tool doesn't have to be super complicated to be tremedously useful.

Tags: tyska