# Per-Directory Environments With direnv

> Load and unload environment variables per directory with direnv, and pull each project's secrets straight from 1password with op read.

- Author: Ross Edman
- Published: 2026-10-09
- Tags: shell, tooling
- URL: https://rossedman.io/blog/computers/per-directory-environments-with-direnv/

**TL;DR:** direnv loads and unloads environment variables as you `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](https://rossedman.io/blog/computers/setting-env-vars-from-1password/) I mentioned [`direnv`](https://direnv.net/) 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

**macOS**

```shell
brew install direnv
```

**Ubuntu**

```shell
sudo apt install direnv
```

direnv works by hooking into your shell prompt, so you need to add one line to your `~/.zshrc` (or `~/.bashrc`, swap `zsh` for `bash`)

```shell
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.

```shell
➜ 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 content
```

Wait, 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.

```shell
➜ direnv allow
direnv: loading ~/code/demo/.envrc
direnv: export +GREETING

➜ echo $GREETING
Hello, World!

➜ cd ..
direnv: unloading

➜ echo $GREETING

```

Loaded 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.

```shell
PATH_add bin
```

`dotenv_if_exists` loads a `.env` file if one is there. Lots of projects already have these, so you don't have to rewrite them.

```shell
dotenv_if_exists .env
```

`source_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.

```shell
source_up
export APP_ENV=development
```

## Secrets 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.

```shell
➜ op read "op://Work/github-pat/password"
some-random-long-password-string
```

> **Tip.** In the 1password app, right click any field and pick **Copy Secret Reference** to get the exact `op://` path.
Now 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/

```shell
export GITHUB_TOKEN="$(op read "op://Work/github-pat/password")"
```

```shell
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`

```shell
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

**Do**

- \+ Add `.envrc` to your global gitignore
- \+ Use `op read` so secrets never touch disk
- \+ Use `source_up` to stack settings

**Don't**

- − Commit an `.envrc` that holds secrets
- − Run `direnv allow` on 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!
