direnv: Tree-Based Environment State for Your Terminal
Every project needs different environment variables. GitHub credentials for personal projects. GitHub Enterprise for work. Different AWS profiles. Different API keys. Different Node.js versions.
The traditional approach: manually export variables, or source project-specific shell scripts, or maintain complex .zshrc configurations that load everything globally.
direnv automates this. Drop a .envrc file in any directory. direnv loads it when you cd in, unloads it when you cd out. Tree-based: child directories inherit parent .envrc settings. Works with any shell.
Installation and Setup#
# Install (macOS)
brew install direnv
# Hook into shell (add to ~/.zshrc or ~/.bashrc)
eval "$(direnv hook zsh)"
# or for bash:
eval "$(direnv hook bash)"
Restart your shell. direnv is now active.
Basic Usage#
Create .envrc in any directory:
# ~/projects/personal/.envrc
export GITHUB_USER="robertmeta"
export AWS_PROFILE="personal"
export OPENAI_API_KEY="sk-personal-key-here"
Allow the file:
cd ~/projects/personal
direnv allow
Now whenever you cd into this directory, those variables load automatically. cd out, they unload.
$ cd ~/projects/personal
direnv: loading ~/projects/personal/.envrc
direnv: export +GITHUB_USER +AWS_PROFILE +OPENAI_API_KEY
$ echo $AWS_PROFILE
personal
$ cd ~
direnv: unloading
$ echo $AWS_PROFILE
# (empty - variable unloaded)
Tree-Based Hierarchy#
direnv walks UP the directory tree, loading .envrc files as it finds them. Child settings override parent settings.
~/projects/.envrc # Base config for all projects
~/projects/personal/.envrc # Personal GitHub + AWS
~/projects/cms/.envrc # GitHub Enterprise for CMS
~/projects/cms/api/.envrc # API-specific overrides
When you cd ~/projects/cms/api, direnv loads all three in order: base → cms → api. Child variables override parent variables.
Real-World Pattern: Multiple GitHub Accounts#
Personal projects use github.com. Work projects use GitHub Enterprise at github.cms.gov.
# ~/projects/personal/.envrc
export GH_CONFIG_DIR="$HOME/.config/gh-personal"
export GH_HOST="github.com"
# ~/projects/cms/.envrc
export GH_CONFIG_DIR="$HOME/.config/gh-cms"
export GH_HOST="github.cms.gov"
Setup GitHub CLI configs:
# Authenticate personal account
cd ~/projects/personal
gh auth login
# Authenticate work account
cd ~/projects/cms
gh auth login
Now gh commands automatically use the right account based on your directory:
$ cd ~/projects/personal
$ gh auth status
# Shows: robertmeta@github.com
$ cd ~/projects/cms
$ gh auth status
# Shows: robert.melton@github.cms.gov
Configure git to use gh as credential helper:
gh auth setup-git
This adds to ~/.gitconfig:
[credential "https://github.com"]
helper = !/opt/homebrew/bin/gh auth git-credential
Now git operations automatically use the right credentials based on directory.
Real-World Pattern: AWS Profiles#
Different projects use different AWS accounts:
# ~/projects/personal/.envrc
export AWS_PROFILE="personal"
export AWS_REGION="us-east-1"
# ~/projects/cms/.envrc
export AWS_PROFILE="cms-dev"
export AWS_REGION="us-gov-west-1"
AWS CLI and SDKs automatically use the correct profile:
$ cd ~/projects/personal
$ aws s3 ls
# Lists personal account buckets
$ cd ~/projects/cms
$ aws s3 ls
# Lists CMS account buckets
Real-World Pattern: API Keys#
Work OpenAI key for production. Personal OpenAI key for experiments.
# ~/projects/personal/.envrc
export OPENAI_API_KEY="sk-personal-..."
export OPENAI_ORG_ID="org-personal-..."
# ~/projects/cms/.envrc
export OPENAI_API_KEY="sk-cms-work-..."
export OPENAI_ORG_ID="org-cms-..."
Code that calls OpenAI API automatically uses the right credentials based on directory.
Real-World Pattern: Node.js Versions with nvm#
Old project requires Node 12. New project uses Node 20.
# ~/projects/old-app/.envrc
use node 12
# ~/projects/new-app/.envrc
use node 20
The use node command comes from direnv stdlib. It automatically switches Node versions when you enter the directory.
$ cd ~/projects/old-app
direnv: loading .envrc
direnv: using node 12
$ node --version
v12.22.12
$ cd ~/projects/new-app
direnv: loading .envrc
direnv: using node 20
$ node --version
v20.10.0
Security Model#
direnv won’t load .envrc files automatically. You must explicitly allow them:
$ cd project-with-new-envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.
$ direnv allow
direnv: loading .envrc
direnv: export +GITHUB_USER
This prevents malicious .envrc files from arbitrary repositories auto-loading when you cd into them.
When you edit .envrc, direnv detects the change and blocks it again until you re-allow:
$ vim .envrc
# (make changes)
$ cd .
direnv: error .envrc file has changed. Run `direnv allow` to approve.
$ direnv allow
direnv: loading .envrc
Useful stdlib Functions#
direnv provides helper functions for common patterns:
dotenv - Load .env files:
# .envrc
dotenv
# Now loads .env file if it exists
layout python - Setup Python virtual environment:
# .envrc
layout python python3.11
# Creates/activates venv automatically
PATH_add - Add directory to PATH:
# .envrc
PATH_add bin
PATH_add scripts
# Adds ./bin and ./scripts to PATH
source_up - Inherit parent .envrc:
# .envrc
source_up
# Load parent directory's .envrc first
export PROJECT_SPECIFIC="value"
watch_file - Reload when file changes:
# .envrc
watch_file requirements.txt
# Reload .envrc when requirements.txt changes
Example: Complete Project Setup#
# ~/projects/cms/api/.envrc
# Inherit parent cms settings
source_up
# Load .env file if it exists
dotenv_if_exists
# Use specific Node version
use node 20
# Activate Python venv for scripts
layout python python3.11
# Add local bins to PATH
PATH_add bin
PATH_add node_modules/.bin
# Project-specific variables
export DATABASE_URL="postgresql://localhost/api_dev"
export REDIS_URL="redis://localhost:6379"
export LOG_LEVEL="debug"
# Watch for dependency changes
watch_file package.json
watch_file requirements.txt
When you cd ~/projects/cms/api:
- Loads parent cms config (GitHub Enterprise, AWS profile)
- Loads local
.envif present - Switches to Node 20
- Activates Python venv
- Adds local bins to PATH
- Sets database and Redis URLs
- Sets log level
- Watches for dependency changes
Leave the directory, everything unloads cleanly.
Debugging#
Check current environment:
$ direnv status
Found RC path /Users/rmelton/projects/cms/api/.envrc
Found watch: /Users/rmelton/projects/cms/api/.envrc
Found watch: /Users/rmelton/projects/cms/api/package.json
Loaded 12 variables
See what would be exported:
$ direnv export bash
export AWS_PROFILE=cms-dev
export GITHUB_USER=rmelton
export NODE_VERSION=20
...
Reload manually after changes:
$ direnv reload
When to Use direnv#
Use direnv when:
- Multiple projects need different credentials
- Different language/tool versions per project
- Project-specific environment variables
- Want automatic activation without manual sourcing
Skip direnv when:
- Single project, single environment
- Global tools that should work everywhere
- Secrets that shouldn’t be in files (use password manager)
Common Patterns#
Parent + child hierarchy:
~/projects/.envrc # Shared tooling paths
~/projects/work/.envrc # Work credentials
~/projects/work/api/.envrc # API-specific config
Per-environment configs:
~/projects/app/.envrc # Development defaults
~/projects/app/.envrc.staging # Staging overrides
~/projects/app/.envrc.prod # Production overrides
Load via: source_env .envrc.staging in your main .envrc.
Conditional loading:
# .envrc
if [ -f .env.local ]; then
dotenv .env.local
fi
Integration with Other Tools#
direnv works with:
- nvm -
use node <version> - pyenv -
layout python <version> - rbenv -
layout ruby <version> - go -
layout go - AWS CLI -
export AWS_PROFILE - gh CLI -
export GH_CONFIG_DIR - kubectl -
export KUBECONFIG
Any tool that reads environment variables automatically adapts to your current directory.
Available at direnv.net with installation guides for all platforms.