direnv: Convenient project-specific environments
I’m pretty particular about my development tools, and I really dislike any tool that requires careful curation of global state - e.g. ruby gems, python packages, etc. In recent years, things have gotten better. Ruby has
bundler, which keeps a project’s dependencies locally (avoiding any global state). Similarly, python has
virtualenv, which does much the same thing. Tools like
nvm allow you to manage multiple versions of the language itself. Notably, the
npm package manager for nodejs fully embraces local dependencies - by default, packages are always installed locally (although the implementation itself is not particularly sane).
The inconvenience with most of these is that they require the developer to do something to “get into” a certain environment - if you try to run your python project’s tests without first activating the correct virtualenv, things will fail pretty badly. Some tools (e.g
rvm) include shell hooks to automatically activate the appropriate environment when you change directories, but they are tool-specific - you’ll need to add a hook in your shell for each tool you use, and I have my doubts that they would cooperate well since they do awful things like overriding the
I was very excited to find out about direnv (github: zimbatm/direnv) a few weeks ago, because I had just been looking for exactly such a tool, and considering writing one myself (I’m rather glad I didn’t have to). The idea is simple: extract all the messy stuff that
virtualenv, etc do to manage per-directory environment variables, and put it into a single, general-purpose tool. You place an
.envrc script in the root directory of your project, and you can use whatever tools you need to inside that script to set project-specific environment variables (via
export statements, or by delegating to bundler, virtualenv, etc).
direnv takes care of sandboxing these modifications so that all changes are reversed when you leave the project directory.
Aside from relieving other tools of the arduous work of reimplementing this particular wheel (including individual integration with each shell), direnv is much more extensible than existing language-specific tools - you can (for example) also export credentials like
AWS_ACCESS_KEY, or add project-specific scripts to your
$PATH so you can just run
mk, rather than having to invoke an explicit path like
Of course, few tools get my blessing these days if they don’t play well with ZeroInstall (if I had my way, all of
rvm/virtualenv/npm/pip would be replaced by just using ZeroInstall, but sadly I have yet to convince everyone to do that ;)). A while ago I wrote 0env as a tool for making ZeroInstall dependencies available in your shell, but unlike most tools it encourages you to work in a subshell, rather than altering your current shell session. Some people don’t like this approach, but the benefits (in code simplicity and lack of bugs) were well worth it. Thankfully, you can have your cake and eat it too if you use
direnv. For example, a normal use of
0env looks like:
$ 0env myproject.xml [myproject] $ # I'm in a subshell [myproject] $ exit $ # back in my original shell
But for convenience, you can make a trivial .envrc that defers all the logic to
$ cat .envrc direnv_load 0env myproject.xml -- direnv dump
Now, every time you
cd into this project directory, direnv will set up whatever environment variables
0env would have set in the subshell, but it applies them to your current session instead, making sure to revert them when you leave the project directory.
Obviously, care should be taken when automatically running scripts, since just cloning some code to your computer should not imply that you trust it to run arbitrary code.
direnv is pretty respectable here: an
.envrc will only be loaded once you’ve explicitly allowed it (by calling
direnv allow in the directory). An
allow action records the full path to the .envrc as well as a hash of its current contents - direnv will refuse to run every
.envrc that doesn’t have a matching allow rule for both of these properties (i.e if it’s changed or has been moved / copied).
There are still potential attacks - e.g if I add
$PATH, then someone could create a pull request with add a malicious
ls script in
./tools. If I check it out locally, neither the
.envrc nor the location has changed, so
direnv would run the .envrc, and then I’d be in trouble then next time I run
ls (I do that a lot). This is pretty hard to avoid in the general case, I think the best approach is to keep the
.envrc simple and as specific as possible, so that there is as most one place where bad things could happen, which you just have to be mindful of (e.g I’d be very cautious of any change which added new files under
tools/ in the above example).
Development and contributing
I’m using direnv
2.2.1, which is barely a week old. It includes both of the features I contributed, which I (obviously ;)) think are important:
direnv_loadcommand (used above), and
- adding the file path to the
direnv allowrules - previously, a
.direnvwould be executed if it had identical contents to an allowed script, regardless of location.
The author (zimbatm) seems friendly and receptive to patches, which makes contributing to direnv pretty painless. It’s written in
go, which I’ve never used before. I’m definitely not a fan of the language’s insistence that error conditions must be implemented by wrapping almost every single function call in an
if block (but which doesn’t even warn you if you completely ignore a function’s returned error value), but aside from that the
direnv code is quite simple and easy to work with. And it’s certainly a huge step up from
bash, which is what it used to be written in, and which many similar tools are written in.