$linuxjunkies
>

Use direnv for Per-Project Environments

Learn to use direnv to automatically load per-project environment variables, with integration guides for pyenv, nvm, mise, and asdf.

BeginnerUbuntuDebianFedoraArch8 min readUpdated June 7, 2026

Before you start

  • bash, zsh, or fish shell configured with a personal RC file
  • Basic familiarity with exporting environment variables in shell scripts
  • A version manager (nvm, pyenv, or mise) already installed if you intend to use those integrations

direnv automatically loads and unloads environment variables when you change directories. Instead of sourcing activation scripts by hand or polluting your global shell environment, you drop a .envrc file in a project root and direnv handles the rest. It works with any shell, any language toolchain, and composes cleanly with version managers like nvm, pyenv, mise, and asdf.

Install direnv

Install from your distribution's package manager. The package name is consistently direnv across distros.

Debian / Ubuntu:

sudo apt install direnv

Fedora / RHEL 9+ / Rocky:

sudo dnf install direnv

Arch:

sudo pacman -S direnv

If your distro ships an older version and you need a recent release, the official install script fetches the latest binary directly:

curl -sfL https://direnv.net/install.sh | bash

Hook direnv Into Your Shell

direnv requires a shell hook that runs after every prompt. Add exactly one line to your shell's RC file, then restart the shell or source the file.

bash — append to ~/.bashrc:

eval "$(direnv hook bash)"

zsh — append to ~/.zshrc:

eval "$(direnv hook zsh)"

fish — append to ~/.config/fish/config.fish:

direnv hook fish | source

After saving, reload:

source ~/.bashrc   # or ~/.zshrc, or restart your terminal

Create Your First .envrc

Navigate to a project directory and create a .envrc file. This is a plain shell script that direnv executes in a sandbox and exports the resulting variables into your shell.

cd ~/projects/myapp
cat > .envrc <<'EOF'
export APP_ENV=development
export DATABASE_URL=postgres://localhost/myapp_dev
export PORT=3000
EOF

Allow the File

direnv refuses to load a .envrc it hasn't seen before. This is a security measure — it prevents a cloned repo from silently executing arbitrary code in your shell. After reviewing the file, explicitly allow it:

direnv allow .

You'll see a message like direnv: loading ~/projects/myapp/.envrc. Leave the directory and return to confirm load/unload works:

cd /tmp && echo $APP_ENV   # empty
cd ~/projects/myapp && echo $APP_ENV   # development

Any time you edit .envrc, direnv blocks it again until you re-run direnv allow. Use direnv deny . to revoke a directory you previously allowed.

The stdlib: Built-in Helpers

direnv ships a standard library of shell functions available inside every .envrc. These cover the most common patterns without extra tooling.

  • dotenv — load a .env file (key=value format) into the environment
  • layout python3 — create and activate a virtualenv automatically
  • layout node — prepend node_modules/.bin to PATH
  • path_add PATH ./bin — safely prepend a local directory to PATH
  • source_env .envrc.local — include another envrc file (useful for secrets not committed to git)

A Python project that uses a local virtualenv looks like this:

cat > .envrc <<'EOF'
layout python3
export DJANGO_SETTINGS_MODULE=config.settings.dev
EOF
direnv allow .

direnv creates .direnv/python-3.x.x/, builds a virtualenv there, and activates it on entry. You never call source .venv/bin/activate again.

Integrating With pyenv

If you manage Python versions with pyenv, combine it with the layout pyenv helper (available in newer direnv builds) or call pyenv local alongside layout python3:

cat > .envrc <<'EOF'
export PYENV_VERSION=3.11.9
layout python3
EOF
direnv allow .

direnv will pick up whichever Python pyenv resolves for that version string. Make sure pyenv's shims are in PATH before the direnv hook runs — your ~/.bashrc or ~/.zshrc pyenv initialization should come before the eval "$(direnv hook ...)" line.

Integrating With nvm

nvm is a shell function, not a binary, which makes it slightly awkward to call from inside .envrc. The recommended pattern uses use nvm from direnv-nvm, or you can source nvm directly:

cat > .envrc <<'EOF'
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
nvm use 20
EOF
direnv allow .

This works but adds latency because sourcing nvm.sh is slow. The faster modern approach is to switch to mise (see below), which was built with direnv integration in mind.

Integrating With mise (or asdf)

mise is a fast, modern runtime version manager (compatible with .tool-versions files used by asdf). It has first-class direnv integration.

mise + direnv

After installing mise, activate its direnv integration once:

mise settings set experimental true
mise direnv activate

Then in any project with a .tool-versions or mise.toml file, add a single line to .envrc:

cat > .envrc <<'EOF'
use mise
EOF
direnv allow .

mise reads your .tool-versions (e.g., nodejs 20.14.0, python 3.12.3) and shims the correct binaries into your PATH for that directory only.

asdf (classic)

For asdf users, the pattern is similar — source the asdf script inside .envrc:

cat > .envrc <<'EOF'
source "$(brew --prefix asdf)/libexec/asdf.sh" 2>/dev/null \
  || source "$HOME/.asdf/asdf.sh"
use asdf
EOF
direnv allow .

Keep Secrets Out of Version Control

Add .envrc to your .gitignore if it contains secrets, or use a two-file pattern: commit a .envrc with non-sensitive settings and load a gitignored .envrc.local for secrets:

# .envrc  (committed)
layout python3
export APP_ENV=development
source_env_if_exists .envrc.local
# .envrc.local  (gitignored)
export SECRET_KEY=s3cr3t
export STRIPE_API_KEY=sk_test_...
echo ".envrc.local" >> .gitignore

Verify Everything Is Working

direnv status

Output (will vary) shows the found .envrc path, whether it is allowed, and the last load time. Use direnv reload to force a fresh load without leaving the directory.

Troubleshooting

Variables aren't loading

  • Confirm the hook line is in your RC file and your shell was reloaded.
  • Run direnv status — a "not allowed" state means you need direnv allow ..
  • Check for syntax errors: bash -n .envrc (or zsh -n).

"command not found" for nvm or pyenv inside .envrc

Shell functions defined in your RC file are not available in direnv's sandbox. You must source the relevant init script explicitly inside .envrc, or use mise/asdf which are real binaries.

Slow shell prompt after adding direnv

Usually caused by sourcing a heavy script (nvm.sh, conda activate) inside .envrc. Switch to mise for version management — its direnv path injection is near-instant compared to sourcing nvm.

direnv blocks after git pull

If a teammate changed .envrc, direnv blocks it deliberately. Review the diff (git diff HEAD~1 .envrc), then direnv allow . once you're satisfied.

tested on:Ubuntu 24.04Fedora 40Arch rollingDebian 12

Frequently asked questions

Is it safe to commit .envrc to git?
It depends on what's in it. Layout directives and non-sensitive exports are fine to commit. API keys, passwords, and tokens belong in a gitignored .envrc.local loaded via source_env_if_exists.
Why does direnv block my .envrc after I edit it?
direnv re-blocks any file that changes as a security measure, preventing a git pull from silently executing new code. Review the change and run direnv allow . again to re-authorize it.
Can I use direnv with conda environments?
Yes. Source the conda.sh init script inside .envrc and call conda activate myenv. Be aware this adds noticeable latency; mise with a Python plugin is faster for most use cases.
Does direnv work inside Docker or CI pipelines?
direnv is primarily a developer workstation tool. In CI, set environment variables through your pipeline's native secret/env mechanism instead. direnv can technically run in CI but adds unnecessary complexity.
What is the difference between mise and asdf for use with direnv?
Both read .tool-versions files and manage language runtime versions. mise is a single Rust binary that is significantly faster to activate and has a dedicated use mise directive for direnv. asdf is the original tool and has broader community plugins but slower shell startup.

Related guides