Logo
BlogDashboardAboutProjects
Logo
HomeBlogDashboardAboutProjects
GithubGithub
LinkedinLinkedin
TwitterTwitter
v1.12.1
—
21 Apr 2026
·
3 min read

Per-project shell environments with direnv

Loading the right environment variables when I cd into a project used to mean sourcing .env files by hand or leaking secrets into my global shell. direnv fixes both by loading a per-directory shell environment, and unloading it the moment I leave.

I work across a handful of projects in a day. Each one has its own database URL, API keys, and sometimes its own Node version. Sourcing .env files by hand is tedious, and exporting secrets into my global shell is a bad idea.

direnv loads a per-directory shell environment when I cd in, and unloads it when I leave. That's the whole pitch.

What direnv does

direnv hooks into your shell. Every time you change directory, it walks up the tree looking for an .envrc file. If it finds one, and you've explicitly allowed it, direnv runs the file and injects the resulting environment into your current shell.

Leave the directory, and those variables are gone. No manual unset. No stale DATABASE_URL pointing at the wrong project.

Installing it

On macOS with Homebrew:

brew install direnv

Then hook it into your shell. For zsh, add this to the bottom of ~/.zshrc:

eval "$(direnv hook zsh)"

For bash, use ~/.bashrc and direnv hook bash. Restart your shell.

Basic usage

Drop an .envrc into a project:

export DATABASE_URL="postgres://localhost:5432/blog"
export NODE_ENV="development"

cd into the directory and direnv will refuse to run it until you approve:

direnv: error .envrc is blocked. Run `direnv allow` to approve its content

Run direnv allow. From now on, every time you enter that directory, those variables are exported. Leave, and they're unset.

The approval is the safety layer. If someone else edits the .envrc, whether a teammate, a dependency, or a malicious patch, direnv blocks it again until you re-approve.

Loading an existing .env file

Most of my projects already have a .env file that other tools read. I don't want to duplicate values. .envrc can source them:

dotenv

That's it. direnv reads .env and exports everything. If the file doesn't exist, direnv complains. Use dotenv_if_exists for optional loading.

You can also point at a different file:

dotenv .env.local

Keeping secrets out of git

Add .envrc to your global gitignore, not the repo's:

.envrc
.direnv/

The repo stays clean for teammates who don't use direnv. Your per-machine setup stays local.

If the .envrc itself is safe to share (no secrets, just dotenv and tooling setup), commit it. Put the actual secrets in .env.local and gitignore that instead.

What else it can do

direnv isn't just for environment variables. The .envrc is a shell script, so anything you'd put in .bashrc works:

  • Add a project-local bin/ to PATH. PATH_add bin puts ./bin in front, scoped to this directory.
  • Pin a Node version. use node 22.11.0 via the node layout.
  • Activate a Python virtualenv. layout python3 creates and activates one per project.

I mostly use it for environment variables and the occasional PATH_add. The rest is there when I need it.

Takeaways

  • Secrets stay scoped to the project that needs them.
  • No more source .env muscle memory.
  • The direnv allow gate means you approve every change before it runs.

If you already juggle a few projects with different env files, direnv pays for itself in a week.

Related Articles

26 Dec 2022
2 tags
Different Git identities for personal and work projects
I kept committing with my personal email on work projects. Git's includeIf directive fixes this by loading a different config based on which directory you're in. No manual switching.
24 Oct 2025
2 tags
How I use GitHub CLI day-to-day
I keep most of my work inside the terminal. Here's how I use gh to create repos, track issues, and merge pull requests without touching the GitHub website.
26 Feb 2023
1 tag
Setting 'main' as your default Git branch
GitHub switched the default branch name from master to main in 2020. One line in your global git config means you never have to set it manually on a new repo again.
29 Mar 2023
1 tag
Feature flags: ship code without shipping features
A feature flag is an if/else around a piece of code, controlled by an environment variable. It lets you merge and deploy work-in-progress without turning it on. Fewer conflicts, safer releases.