Skip to content

Fix: direnv Not Working — Hook Activation, .envrc Allow, Layouts, and Editor Integration

FixDevs ·

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 content

Or 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.
  • .envrc must be allowed. New or modified .envrc files are blocked by default. You run direnv allow to 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 python runs python -m venv and exports VIRTUAL_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 | source

PowerShell ($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 +FOO

Pro 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 +FOO

This 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 content

For 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 allow

Common 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 .env

dotenv 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.development

dotenv_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/.bin

PATH_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_test

source_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.12

This 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/activate

For Node.js with nvm:

# .envrc
use nvm   # Loads the version specified in .nvmrc

For Ruby:

# .envrc
use rbenv   # Loads the version specified in .ruby-version

Common 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 it

Fix 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 .envrc

Or force reload:

direnv reload

reload 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/.envrc

To 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.local

The 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=1

Fix 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_KEY
docker 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 loading but the env var isn’t visible. Some shells (older fish, certain zsh themes) interfere with the prompt hook. Update your shell or check that direnv hook is the last line in your init file.
  • .envrc works in subdirectory but not the parent. direnv only loads the closest .envrc by default. Use source_up in child to inherit parent.
  • Slow cd due to direnv. Your .envrc does heavy work (running npm install, building venvs). Move expensive setup to a make setup target; keep .envrc to 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_add doesn’t seem to work. It prepends to PATH but only inside direnv’s loaded context. If you call direnv exec . echo $PATH, you should see the prepended dir.
  • Slow Python venv on macOS. layout python rebuilds 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, the use flake directive is best. Requires nix-direnv plugin.
  • Multiple .envrc files in a path. direnv loads the closest parent. If a parent’s .envrc set DATABASE_URL and the closer one doesn’t, direnv unsets it on entry. Use source_up to inherit.

For related shell and env-loading issues, see dotenv not loading, Env variable undefined, mise not working, and Python virtualenv wrong python.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles