Per-Directory Environments With direnv
cd around. Combine it with op read and every project gets the right secrets, PATH and kubeconfig without you thinking about it.At the end of my 1password post I mentioned direnv almost as an afterthought. Since then it has become one of the first things I install on a new machine, so it deserves its own post.
The idea is simple. You put an .envrc file in a directory. When you cd into that directory (or anything under it), direnv loads it. When you cd out, it unloads everything it set. That’s it. No more source .env and forgetting to undo it, no more exporting the wrong GITHUB_TOKEN into the wrong repo.
In this post I will show you how I set it up, the parts of its standard library I actually use, and an updated version of the 1password trick. Let’s dive into it.
Installing
brew install direnvsudo apt install direnvdirenv works by hooking into your shell prompt, so you need to add one line to your ~/.zshrc (or ~/.bashrc, swap zsh for bash)
eval "$(direnv hook zsh)"Open a new terminal and you’re ready.
Your First .envrc
Let’s make a project and give it an environment variable.
➜ mkdir -p ~/code/demo && cd ~/code/demo
➜ echo 'export GREETING="Hello, World!"' > .envrc
direnv: error /Users/redman/code/demo/.envrc is blocked. Run `direnv allow` to approve its contentWait, blocked? This is my favorite part of direnv. It will never run an .envrc it hasn’t seen before, and it blocks it again any time the file changes. Imagine cloning a repo with an .envrc that does something nasty. direnv makes you look first.
➜ direnv allow
direnv: loading ~/code/demo/.envrc
direnv: export +GREETING
➜ echo $GREETING
Hello, World!
➜ cd ..
direnv: unloading
➜ echo $GREETINGLoaded on the way in, gone on the way out. Wow. Much amaze.
The Standard Library
.envrc files are just bash, but direnv ships a standard library of helpers that make them a lot nicer. These are the ones I actually use.
PATH_add puts a directory on your PATH, relative to the .envrc. Great for a repo full of scripts.
PATH_add bindotenv_if_exists loads a .env file if one is there. Lots of projects already have these, so you don’t have to rewrite them.
dotenv_if_exists .envsource_up is the important one. By default direnv only loads the closest .envrc, so a project-level file would hide your folder-level one. source_up loads the parent’s .envrc first, so settings stack.
source_up
export APP_ENV=developmentSecrets From 1password, Updated
In the old post I used op get item ... --fields password. The 1password CLI has had a big version bump since then, and the newer way is much nicer. Every field has a secret reference that looks like a URL, and op read returns just that value.
➜ op read "op://Work/github-pat/password"
some-random-long-password-stringNow combine this with the directory structure from before. Each folder loads the token from its own vault.
- ~/code/
- work/
- .envrc
- api-service/
- github.com/
- .envrc
- rossedman/
- rossedman.github.io/
- work/
export GITHUB_TOKEN="$(op read "op://Work/github-pat/password")"export GITHUB_TOKEN="$(op read "op://Personal/github-pat/password")"And a project inside work/ can add its own settings on top with source_up
source_up
PATH_add bin
export KUBECONFIG="$HOME/.kube/work-staging.yaml"Now cd ~/code/work/api-service gives you the work GitHub token, the repo’s scripts on your PATH and the right cluster, all at once. cd somewhere else and all of it goes away. This is the part that sold me. I have pointed kubectl at the wrong cluster exactly as many times as I am going to.
A Few Gotchas
- + Add
.envrcto your global gitignore - + Use
op readso secrets never touch disk - + Use
source_upto stack settings
- − Commit an
.envrcthat holds secrets - − Run
direnv allowon a file you haven’t read - − Expect aliases or functions to carry over
That last one trips people up. direnv only exports environment variables. Shell aliases and functions defined in an .envrc won’t show up in your shell.
Conclusion
direnv is one of those tools that disappears once it is set up, which is the best compliment I can give a tool. Every project knows its own environment, secrets come straight from 1password, and switching contexts is just cd. If you only take one thing from this post, make it source_up plus op read. Happy hacking!