Fix: direnv Not Working — Hook Activation, .envrc Allow, Layouts, and Editor Integration
Quick Answer
How to fix direnv errors — hook not loaded in shell, .envrc blocked until allow, layout python venv not activated, dotenv loader, environment leaking to parent dirs, and VS Code/JetBrains direnv integration.
The Error
You install direnv but cd-ing into a directory with .envrc does nothing:
$ echo 'export FOO=bar' > .envrc
$ cd .
$ echo $FOO
# Empty — direnv didn't load it.Or direnv loads the file but warns it’s blocked:
direnv: error /path/.envrc is blocked. Run `direnv allow` to approve its contentOr layout python doesn’t activate a venv:
$ cat .envrc
layout python python3.12
$ direnv allow
$ python --version
Python 3.10.4 # System Python — venv didn't activate.Or VS Code’s integrated terminal sees env vars but the language server doesn’t:
Pylance reports "module not found" — even though `python -c "import requests"` works in the same shell.Why This Happens
direnv hooks into your shell’s prompt rendering to detect directory changes. On every cd, it checks for an .envrc in the new directory (and parents), loads them, and unloads when you leave. Three core requirements:
- Shell hook must be installed in your shell’s init file. Without it, direnv is just a binary on PATH that nothing calls.
.envrcmust be allowed. New or modified.envrcfiles are blocked by default. You rundirenv allowto approve — direnv computes a hash and checks it on each load. Edit the file, the hash changes, the block re-engages.- Layouts call external tools.
layout pythonrunspython -m venvand exportsVIRTUAL_ENV. If the requested Python isn’t installed, it fails silently. - IDEs run their own processes that don’t always inherit env from your shell. Direnv-aware extensions or wrappers are needed.
Fix 1: Install the Shell Hook
Add to your shell init:
Bash (~/.bashrc):
eval "$(direnv hook bash)"Zsh (~/.zshrc):
eval "$(direnv hook zsh)"Fish (~/.config/fish/config.fish):
direnv hook fish | sourcePowerShell ($PROFILE):
Invoke-Expression "$(direnv hook pwsh)"Open a new shell. cd into a directory with .envrc — direnv should print:
direnv: loading ~/project/.envrc
direnv: export +FOOPro Tip: Put the eval last in your shell init. direnv wraps your prompt; other tools (like starship or powerlevel10k) that also wrap the prompt should be set up before direnv.
Fix 2: Allow the .envrc
direnv refuses to source files until you explicitly approve them:
$ cd ~/project
direnv: error .envrc is blocked. Run `direnv allow` to approve its content
$ direnv allow
direnv: loading ~/project/.envrc
direnv: export +FOOThis is a security feature. Without it, git clone of a malicious repo with .envrc could exfiltrate your env or run arbitrary code.
When you edit .envrc, the hash changes and direnv re-blocks:
$ echo 'export BAR=baz' >> .envrc
$ cd .
direnv: error .envrc is blocked. Run `direnv allow` to approve its contentFor frequent edits, alias:
alias da='direnv allow'To trust without approving (don’t do this for repos from others):
direnv allow .envrc
# or temporarily:
DIRENV_DISABLE_STDIN=1 direnv allowCommon Mistake: direnv revoke then re-allow. The two-step is rare in practice — direnv allow always replaces the hash. Use revoke only when you want to explicitly deny.
Fix 3: Common .envrc Patterns
For simple env:
# .envrc
export DATABASE_URL=postgres://localhost/myapp
export NODE_ENV=development
export OPENAI_API_KEY=sk-...For loading from a file (gitignored):
# .envrc
dotenv .envdotenv is a direnv built-in that reads a KEY=VALUE file and exports each. The .env file should be in .gitignore; commit .envrc only.
For multi-file:
# .envrc
dotenv .env
dotenv_if_exists .env.local
dotenv_if_exists .env.developmentdotenv_if_exists is the safe form — skips files that don’t exist instead of erroring.
For PATH manipulation:
# .envrc
PATH_add bin
PATH_add ./node_modules/.binPATH_add prepends; PATH_rm removes. Both work on the current PATH.
For child directories inheriting parent .envrc:
# Inside subdirectory's .envrc:
source_up
# Then add subdirectory-specific overrides:
export DATABASE_URL=postgres://localhost/myapp_testsource_up loads the nearest parent .envrc before your own. Useful for monorepos.
Fix 4: layout python and Venv Management
The layout family activates language-specific environments:
# .envrc
layout python python3.12This creates a venv at .direnv/python-3.12/ and activates it. Every python, pip in this directory uses the venv.
For uv-based venvs:
# .envrc
layout python python3.12
# uv reads VIRTUAL_ENV that direnv exports, so uv pip install works inside it.For Poetry projects:
# .envrc
# Activate poetry's venv (assumes poetry has already created it):
source $(poetry env info --path)/bin/activateFor Node.js with nvm:
# .envrc
use nvm # Loads the version specified in .nvmrcFor Ruby:
# .envrc
use rbenv # Loads the version specified in .ruby-versionCommon Mistake: Mixing layout python with manual pyenv activate — both manage a venv, they fight. Pick one.
For mise integration (newer setups):
# .envrc
use mise # Or just let mise's shell hook handle itFix 5: Editor and IDE Integration
direnv runs in your shell — IDEs that spawn their own subprocesses miss the env. Fix per editor:
VS Code:
Install the direnv extension by mkhl. It reads .envrc and applies env to language servers and integrated terminal.
Or configure the terminal to launch a login shell that loads .bashrc:
// .vscode/settings.json
{
"terminal.integrated.profiles.linux": {
"bash-login": {
"path": "bash",
"args": ["-l"]
}
},
"terminal.integrated.defaultProfile.linux": "bash-login"
}JetBrains IDEs: Use direnv plugin from the marketplace, or set “Environment variables” in run configurations manually.
Neovim (with LSP):
-- Activate direnv before LSP starts:
require("direnv").setup()direnv.vim or direnv.nvim plugins handle this.
Pro Tip: Some editors require restarting the language server after .envrc changes. VS Code: Command Palette → “Restart Language Server.”
Fix 6: Reload Without Leaving the Directory
After editing .envrc, direnv reloads on cd .:
$ vim .envrc
$ cd . # Triggers reload
direnv: loading .envrcOr force reload:
direnv reloadreload is useful in long-lived shells where you’ve been working in the same directory and just changed .envrc.
To see what direnv currently has loaded:
direnv status
# direnv exec path /usr/local/bin/direnv
# DIRENV_DIR /path/to/project
# DIRENV_FILE /path/to/project/.envrcTo debug a misbehaving .envrc:
DIRENV_LOG_FORMAT="$(date +%H:%M:%S) [direnv] %s" direnv reload
# Verbose output for what direnv exports/unexports.Fix 7: Standard Patterns
Pattern A — share .envrc, gitignore .env:
# .envrc (committed)
dotenv_if_exists .env
export PROJECT_ROOT=$PWD
PATH_add bin
# .env (gitignored)
DATABASE_URL=postgres://localhost/myapp
API_KEY=secret# .gitignore
.env
.envrc.localThe committed .envrc documents what env vars the project uses; the gitignored .env has the actual values.
Pattern B — .envrc.example documentation:
# .envrc.example (committed)
# Copy to .envrc and fill in values.
export DATABASE_URL=postgres://localhost/myapp
export API_KEY=New contributors copy and edit. Some teams prefer this over a .env file.
Pattern C — .envrc.local for personal overrides:
# .envrc (committed)
dotenv .env
dotenv_if_exists .envrc.local
# .envrc.local (gitignored)
export DEBUG=1Fix 8: CI Integration
direnv isn’t typically run in CI — CI has its own env var injection (GitHub Actions secrets, GitLab variables). But for scripts that depend on .envrc:
# In CI:
direnv exec . <command>
# Loads .envrc, runs the command, then unloads.exec runs in a one-shot direnv context — useful for scripts that need direnv-exported vars without polluting the CI shell.
For Docker builds that need access to env from .envrc:
# Don't COPY .envrc — that's a secret.
# Instead, export at build time:
ARG OPENAI_API_KEY
ENV OPENAI_API_KEY=$OPENAI_API_KEYdocker build --build-arg OPENAI_API_KEY=$OPENAI_API_KEY .Pro Tip: Don’t COPY .envrc /app/ in a Dockerfile. The .envrc may contain secrets in plain text. Use build args or runtime env injection.
Still Not Working?
A few less-obvious failures:
- direnv prints
loadingbut the env var isn’t visible. Some shells (older fish, certain zsh themes) interfere with the prompt hook. Update your shell or check thatdirenv hookis the last line in your init file. .envrcworks in subdirectory but not the parent. direnv only loads the closest.envrcby default. Usesource_upin child to inherit parent.- Slow
cddue to direnv. Your.envrcdoes heavy work (runningnpm install, building venvs). Move expensive setup to amake setuptarget; keep.envrcto fast env exports. - **
direnv: \X’ is unsetwarnings.** A previous.envrc` exported X; the new directory doesn’t, so direnv unexports it. Warning is informational unless you actually need X. PATH_adddoesn’t seem to work. It prepends to PATH but only inside direnv’s loaded context. If you calldirenv exec . echo $PATH, you should see the prepended dir.- Slow Python venv on macOS.
layout pythonrebuilds the venv if Python’s path changes. Pin Python via mise or asdf to keep the venv stable. use_nix/ nix-flake integration. For Nix users, theuse flakedirective is best. Requiresnix-direnvplugin.- Multiple
.envrcfiles in a path. direnv loads the closest parent. If a parent’s.envrcsetDATABASE_URLand the closer one doesn’t, direnv unsets it on entry. Usesource_upto inherit.
For related shell and env-loading issues, see dotenv not loading, Env variable undefined, mise not working, and Python virtualenv wrong python.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Bun Shell Not Working — $ Template Quoting, Pipes, Exit Codes, and Cross-Platform Scripts
How to fix Bun Shell errors — $ template auto-escape vs raw strings, piping with pipe() vs |, throws on non-zero exit, cwd/env scoping, glob expansion differences, and Windows path handling.
Fix: ESLint Flat Config Not Working — eslint.config.js, ignores, Plugins, and Migration
How to fix ESLint flat config errors — eslint.config.js not found, .eslintrc.json ignored after upgrade, ignores replacing .eslintignore, plugin object form, typescript-eslint integration, monorepo configs, and ESLINT_USE_FLAT_CONFIG.
Fix: Lefthook Not Working — Install, Staged Files, Glob Filters, Parallel Runs, and CI Skip
How to fix Lefthook errors — hooks not running after install, {staged_files} empty for new files, glob filter not matching, parallel: true ordering, LEFTHOOK=0 to skip in CI, and lefthook-local.yml overrides.
Fix: mise Not Working — Shell Activation, .tool-versions, Plugin Install, and Python venv
How to fix mise (formerly rtx) errors — activation hook not running, tool not found after install, .tool-versions vs .mise.toml, Python venv integration, idiomatic env loading, and trust prompts.