| name | direnv |
| description | Guide for using direnv - a shell extension for loading directory-specific environment variables. Use when setting up project environments, creating .envrc files, configuring per-project environment variables, integrating with Python/Node/Ruby/Go layouts, working with Nix flakes, or troubleshooting environment loading issues on macOS and Linux. |
direnv Skill
This skill provides comprehensive guidance for working with direnv, covering installation, configuration, stdlib functions, and best practices for per-project environment management.
When to Use This Skill
Use this skill when:
- Installing and configuring direnv on macOS or Linux
- Creating or modifying
.envrc files for projects
- Setting up per-project environment variables
- Configuring language-specific layouts (Python, Node.js, Ruby, Go, Perl)
- Integrating direnv with Nix or Nix Flakes
- Managing secrets and environment configuration for teams
- Troubleshooting environment loading issues
- Creating custom direnv extensions
Core Concepts
What is direnv?
direnv is a shell extension that loads and unloads environment variables based on the current directory. When you cd into a directory with a .envrc file, direnv automatically loads the environment. When you leave, it unloads the changes.
Security Model
direnv uses an allowlist-based security approach:
- New or modified
.envrc files must be explicitly allowed with direnv allow
- Prevents automatic execution of untrusted scripts
- Use
direnv deny to revoke access
How It Works
- Shell hook intercepts directory changes
- Checks for
.envrc file in current or parent directories
- If allowed, executes
.envrc in a bash subshell
- Captures exported variables and applies them to current shell
Installation
macOS (Homebrew - Recommended)
brew install direnv
Linux
sudo apt install direnv
sudo dnf install direnv
sudo pacman -S direnv
curl -sfL https://direnv.net/install.sh | bash
Verify Installation
direnv version
Shell Configuration
Add the hook to your shell's config file. This is required for direnv to function.
Zsh (~/.zshrc)
eval "$(direnv hook zsh)"
With Oh My Zsh:
plugins=(... direnv)
Bash (~/.bashrc)
eval "$(direnv hook bash)"
Important: Place after rvm, git-prompt, and other prompt-modifying extensions.
Fish (~/.config/fish/config.fish)
direnv hook fish | source
After Configuration
Restart your shell:
exec $SHELL
.envrc File Basics
Creating an .envrc
touch .envrc
vim .envrc
Basic Syntax
export NODE_ENV=development
export API_URL=http://localhost:3000
export DATABASE_URL=postgres://localhost/myapp
Allowing the .envrc
direnv allow
direnv allow /path/to/project
direnv deny
Standard Library Functions
direnv includes a powerful stdlib. Always prefer stdlib functions over manual exports.
PATH Management
PATH_add bin
PATH_add node_modules/.bin
PATH_add scripts
path_add PYTHONPATH lib
path_add LD_LIBRARY_PATH /opt/lib
PATH_rm "*/.git/bin"
Environment File Loading
dotenv
dotenv .env.local
dotenv_if_exists .env.local
dotenv_if_exists .env.${USER}
source_env ../.envrc
source_env /path/to/.envrc
source_up
source_env_if_exists .envrc.local
Language Layouts
Node.js:
layout node
Python:
layout python
layout python python3.11
layout python3
layout pipenv
Ruby:
layout ruby
Go:
layout go
Perl:
layout perl
Nix Integration
use nix
use nix shell.nix
use flake
use flake "nixpkgs#hello"
use flake ".#devShell"
For better Nix Flakes support, install nix-direnv:
Version Managers
use rbenv
use node 18
use node 18.17.0
use node
use julia 1.9
Validation
env_vars_required API_KEY DATABASE_URL SECRET_KEY
direnv_version 2.32.0
if on_git_branch main; then
export DEPLOY_ENV=production
fi
if on_git_branch develop; then
export DEPLOY_ENV=staging
fi
File Watching
watch_file package.json
watch_file requirements.txt
watch_file .tool-versions
watch_file config/*.yaml
watch_dir config
watch_dir migrations
Utility Functions
if has docker; then
export DOCKER_HOST=unix:///var/run/docker.sock
fi
expand_path ./bin
find_up package.json
strict_env
load_prefix /usr/local/custom
source_url https://example.com/script.sh "sha256-HASH..."
Best Practices
Recommended .envrc Template
#!/usr/bin/env bash
direnv_version 2.32.0
dotenv_if_exists
source_env_if_exists .envrc.local
layout node
PATH_add bin
PATH_add scripts
export NODE_ENV="${NODE_ENV:-development}"
export LOG_LEVEL="${LOG_LEVEL:-debug}"
watch_file package.json
watch_file .nvmrc
Git Configuration
.gitignore:
# Environment files with secrets
.env
.env.local
.envrc.local
# direnv virtualenv/cache
.direnv/
Commit to repository:
.envrc (base configuration, no secrets)
.env.example (template for team members)
Secrets Management
Never commit secrets. Use environment variable fallbacks:
export DATABASE_URL="${DATABASE_URL:-postgres://localhost/dev}"
export API_KEY="${API_KEY:-}"
env_vars_required API_KEY
export DATABASE_URL="postgres://user:secret@prod/app"
export API_KEY="actual-secret-key"
Layered Configuration
export EDITOR=vim
source_up
export API_PORT=3000
source_up
export FEATURE_FLAG=true
Project Structure
my-project/
├── .envrc # Base environment (committed)
├── .envrc.local # Local overrides (gitignored)
├── .env # Environment variables (gitignored)
├── .env.example # Template for team (committed)
└── .direnv/ # direnv cache (gitignored)
Custom Extensions
Create ~/.config/direnv/direnvrc for custom functions:
#!/usr/bin/env bash
use_kubernetes() {
local context="${1:-default}"
export KUBECONFIG="${HOME}/.kube/config"
kubectl config use-context "$context" >/dev/null 2>&1
log_status "kubernetes context: $context"
}
use_aws_secrets() {
local secret_name="$1"
local region="${2:-us-east-1}"
eval "$(aws secretsmanager get-secret-value \
--secret-id "$secret_name" \
--region "$region" \
--query SecretString \
--output text | jq -r 'to_entries | .[] | "export \(.key)=\"\(.value)\""')"
log_status "loaded secrets from: $secret_name"
}
use_asdf() {
watch_file .tool-versions
source_env "$(asdf direnv local)"
}
Usage in .envrc:
use kubernetes dev-cluster
use aws_secrets myapp/dev
use asdf
Commands Reference
| Command | Description |
|---|
direnv allow | Allow the current .envrc |
direnv deny | Revoke .envrc access |
direnv reload | Force reload environment |
direnv status | Show current status |
direnv dump | Dump current environment |
direnv edit | Open .envrc in editor |
direnv version | Show direnv version |
Troubleshooting
Environment Not Loading
direnv status
direnv reload
direnv allow
echo $DIRENV_DIR
Shell Hook Issues
- Verify hook is in shell config file
- Ensure it's at the END of the file
- Restart shell completely:
exec $SHELL
- Check for errors:
direnv hook zsh
Performance Issues
direnv show_dump
Debugging
export DIRENV_LOG_FORMAT='%s'
direnv dump | jq
bash -n .envrc
IDE Integration
VS Code
Install direnv extension for automatic environment loading in integrated terminal.
JetBrains
Install direnv integration plugin.
Neovim
Use direnv.vim or configure with lua.
Common Patterns
Development vs Production
export NODE_ENV="${NODE_ENV:-development}"
if [[ "$NODE_ENV" == "development" ]]; then
export DEBUG=true
export LOG_LEVEL=debug
else
export DEBUG=false
export LOG_LEVEL=info
fi
Multi-Service Projects (Monorepo)
export PROJECT_ROOT="$(pwd)"
export COMPOSE_PROJECT_NAME=myapp
source_up
export SERVICE_NAME=api
export SERVICE_PORT=3000
source_up
export SERVICE_NAME=web
export SERVICE_PORT=8080
Docker Integration
export COMPOSE_FILE=docker-compose.yml
export COMPOSE_PROJECT_NAME="${PWD##*/}"
if has docker-compose; then
export DOCKER_HOST="${DOCKER_HOST:-unix:///var/run/docker.sock}"
fi
PATH_add .docker/bin
References
Gotchas
direnv reloads on cd but NOT on .envrc edit unless you re-enter the dir: A change to .envrc looks applied (no error) but isn't until cd . or direnv reload. Use watch_file on .envrc itself if editing in-place.
direnv allow is keyed on file content hash, not path: Renaming .envrc.dev to .envrc and back keeps allow state. But editing a single byte revokes — even a stray trailing newline from saving in a new editor.
layout python creates .direnv/python-X.Y/ tied to the host's python version: A python upgrade silently breaks the venv. Pin via layout python python3.11 or rebuild with rm -rf .direnv && direnv reload.
source_up searches ancestors, not just immediate parent: A stray .envrc in ~/ or ~/Projects/ leaks into every subdirectory project. Audit with direnv status from deep in a tree to see all chained files.
- Hook placement order matters in
.zshrc: Place eval "$(direnv hook zsh)" AFTER prompt/p10k setup. Earlier and precmd hooks fire before prompt is ready — direnv output gets eaten by the prompt redraw.
PATH_add bin adds project-relative path that breaks when scripts cd elsewhere: The PATH entry is absolute (resolved at load), so cd /tmp && project-binary still works — but binaries that read $PWD/bin at runtime do not.