Introduction
direnv solves a pair of small pains that add up over a career: (1) you cd into a project and forget which env vars that project needs; (2) those env vars leak into unrelated directories. direnv watches your cd, sources a local .envrc when you enter a tracked directory, and unsets the vars when you leave.
With over 15,000 GitHub stars, direnv is the quiet workhorse behind Nix, devenv, mise-managed workflows, and hundreds of thousands of devs' dotfiles. It turns "which env vars do I need again?" into a solved problem.
What direnv Does
When you change directories, direnv checks whether a .envrc exists. If it does and you've approved it with direnv allow, direnv evaluates the file in a subshell and exports its environment into your current shell. When you leave the directory, the changes revert. Its stdlib (use flake, layout python, use asdf) handles common patterns without boilerplate.
Architecture Overview
Shell cd
|
[direnv hook]
runs before/after each prompt
|
[Detect .envrc in cwd or ancestors]
|
[Authorization check]
signature stored in ~/.local/share/direnv/allow/
(so arbitrary repos cannot silently execute code)
|
[Evaluate .envrc in subshell]
stdlib helpers: use_nix, layout node, PATH_add, source_env ...
|
[Diff current env -> target env]
export + unset to reach target
|
[On leaving directory]
undo exports / restore previous valuesSelf-Hosting & Configuration
# .envrc — Python project with venv
layout python3.12
export FLASK_APP=app.py
export FLASK_ENV=development
PATH_add ./scripts
# .envrc — Node project
layout node
use nvm 20
export NODE_ENV=development
dotenv .env # load KEY=value pairs from .env
dotenv_if_exists .env.local
# .envrc — Nix flake
use flake
# .envrc — mix of standard + custom
source_up # inherit parent .envrc first
export DB_URL=$(op read "op://Dev/local-pg/url") # 1Password secret# Handy commands
direnv status # show what will load
direnv reload # re-evaluate
direnv deny # revoke approval (temporarily disable)
direnv allow # approve after editsKey Features
- Per-directory env — exports scoped to a project, cleaned on leave
- Shell hooks — bash, zsh, fish, tcsh, elvish, nu, pwsh
- Stdlib helpers — layout python/node/ruby, use nvm/asdf/nix, PATH_add, dotenv
- Approval model — changes require
direnv allow(protects against drive-by repos) - Source chaining — source_up, source_env, strict_env
- Secret manager integration — pull values from op/aws-vault/bitwarden
- Nix + devenv — first-class support for declarative shells
- Cross-platform — macOS, Linux, Windows (WSL)
Comparison with Similar Tools
| Feature | direnv | dotenv | mise env | shadowenv | Nix shell |
|---|---|---|---|---|---|
| Auto-load on cd | Yes | No | Yes (integrated) | Yes | No (enter shell) |
| Auto-unload on leave | Yes | No | Yes | Yes | Yes (exit shell) |
| Language-agnostic | Yes | Yes | Yes | Yes | Yes |
| Approval required | Yes | No | Partial | Yes | No |
| Complex helpers | Yes (stdlib) | No | Yes | Limited | Yes (flakes) |
| Best For | Universal env switching | Simple KEY=value | mise ecosystem | Shopify-style orgs | Reproducible shells |
FAQ
Q: direnv vs mise?
A: They overlap and complement. mise manages tool versions + env; direnv is env-only. You can use direnv without mise, or use mise with direnv (so mise commands see the right env automatically).
Q: Is it safe?
A: Yes — the approval model stops untrusted repos from running code. Always read .envrc before direnv allow, the same way you'd read a shell script.
Q: What about secrets?
A: Don't check secrets into .envrc. Use dotenv .env.local (gitignored) or helpers like export SECRET=$(op read ...) / export SECRET=$(aws-vault exec ... -- env PRINT_SECRET).
Q: Does direnv slow my shell? A: Virtually no. Its overhead is a few milliseconds per prompt. You won't notice it.
Sources
- GitHub: https://github.com/direnv/direnv
- Website: https://direnv.net
- License: MIT