A developer opens a new terminal tab, runs terraform plan, and gets Error: Unsupported Terraform Core version because the system Terraform ran instead of the project's pinned one — they forgot to run nix develop in that tab. Or direnv is installed but every cd prints direnv: error /home/dev/shop/.envrc is blocked. Run 'direnv allow' to approve its content. direnv makes a reproducible environment automatic: entering the directory loads it, leaving unloads it. This page sets it up properly for a project, including caching and IDE integration, as part of reproducible dev shells with Nix, Devbox and direnv.

direnv works with any environment source — Nix flakes, Devbox, mise, Python virtualenvs, plain export lines — so the same approach applies whichever toolchain manager the repository uses.

Diagnostic

Check whether the hook is installed, whether the project's .envrc is allowed, and which tool a new shell resolves:

#!/usr/bin/env bash
set -euo pipefail
direnv version
grep -n 'direnv hook' ~/.zshrc ~/.bashrc 2>/dev/null || echo "direnv hook not found in shell rc files"
direnv status | grep -E 'Found RC path|Found RC allowed|Loaded RC path' || true
command -v terraform
terraform version | head -1

Expected bad output:

2.34.0
direnv hook not found in shell rc files
Found RC path /home/dev/shop/.envrc
Found RC allowed false
/usr/local/bin/terraform
Terraform v1.5.7

direnv is installed but the shell hook is missing, so nothing runs on cd; the .envrc exists but was never allowed; and the system Terraform is on PATH.

Why the Project Environment Did Not Load Decision diagram mapping direnv status to the missing piece. Why the Project Environment Did Not Load What does direnv status show? no hook in rc file add direnv hook RC allowed false run direnv allow allowed, wrong tool fix .envrc source
The hook, the allow step and the .envrc contents are the three things to check, in that order.

Root cause

direnv has two parts: a binary that evaluates .envrc files, and a shell hook that calls it before each prompt to compare the current directory with the last one. Without the hook, the binary never runs. With the hook, direnv refuses to execute any .envrc it has not seen before, or whose contents changed since the last approval, because .envrc is arbitrary shell code pulled in with every git pull — the allow step is a security control, not an annoyance. Once allowed, direnv evaluates the file in a subshell, captures the resulting environment diff and applies it to your shell. Without caching, use flake re-evaluates the flake on each change of directory, which can take several seconds; nix-direnv caches the result and makes it instant.

IDEs add one more layer. A terminal inside VS Code has the hook, but the IDE's language servers, debuggers and test runners are launched by the IDE process, which never ran .envrc. They see the system tools unless an extension loads direnv's environment into the IDE itself.

Resolution

  1. Install the hook for the shell you use and restart it:
#!/usr/bin/env bash
set -euo pipefail
case "${SHELL##*/}" in
  zsh)  grep -q 'direnv hook zsh' ~/.zshrc || echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc ;;
  bash) grep -q 'direnv hook bash' ~/.bashrc || echo 'eval "$(direnv hook bash)"' >> ~/.bashrc ;;
  fish) mkdir -p ~/.config/fish/conf.d && echo 'direnv hook fish | source' > ~/.config/fish/conf.d/direnv.fish ;;
esac
echo "restart the shell or open a new tab"
  1. Install nix-direnv so flake environments are cached and registered as garbage-collection roots:
#!/usr/bin/env bash
set -euo pipefail
nix profile install nixpkgs#nix-direnv
mkdir -p ~/.config/direnv
grep -q nix-direnv ~/.config/direnv/direnvrc 2>/dev/null || \
  echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' >> ~/.config/direnv/direnvrc
  1. Write a small, reviewable .envrc:
#!/usr/bin/env bash
set -euo pipefail
cat > .envrc <<'EOF'
watch_file flake.nix flake.lock
use flake
export COMPOSE_PROJECT_NAME=shop
dotenv_if_exists .env.local
EOF
direnv allow

watch_file makes direnv reload when the flake changes. dotenv_if_exists layers personal, git-ignored overrides without anyone editing the shared file. For Devbox, replace use flake with the output of devbox generate direnv; for mise, use use mise.

  1. Connect the IDE. In VS Code, install the mkhl.direnv extension; in JetBrains IDEs, the Direnv integration plugin. Both load the .envrc environment into the IDE process so language servers resolve the pinned tools.
direnv on Each Prompt Ordered steps direnv's hook performs before each prompt in a project directory. direnv on Each Prompt 1 — hook runs before prompt 2 — find nearest .envrc 3 — check it is allowed content hash 4 — load cached env or evaluate 5 — apply diff; unload on exit
The allow check is the security gate; the cache makes the rest near-instant.

Expected output

$ cd ~/src/shop
direnv: loading ~/src/shop/.envrc
direnv: using flake
direnv: nix-direnv: using cached dev shell
direnv: export +COMPOSE_PROJECT_NAME +IN_NIX_SHELL ~PATH
$ command -v terraform
/nix/store/0pqh…-terraform-1.8.5/bin/terraform
$ cd ..
direnv: unloading
$ command -v terraform
/usr/local/bin/terraform

Entering the directory loads the pinned Terraform from the Nix store in well under a second; leaving restores the system one.

The export +COMPOSE_PROJECT_NAME ~PATH line is worth reading every time: + marks variables direnv added, - marks variables it removed, and ~ marks variables it modified. If a variable you expect is missing from that line, the .envrc did not set it, which is faster to spot than debugging the tool that needed it. Subdirectories inherit the environment — cd api/ keeps it loaded — and a nested .envrc in a subdirectory can add service-specific settings on top with source_up, which is useful in monorepos where one service needs an extra tool or variable.

A final check is the IDE. Open the project, then open the IDE's own integrated terminal and a run configuration for the test suite, and print the tool path in each. All three should point into the Nix store or .devbox. When only the IDE disagrees, the direnv extension is not active for that workspace — usually because it was installed in a different VS Code profile or in the local context of a remote session.

Prevention

  1. Check the environment in make doctor. A test that the resolved tool path starts with /nix/store/ (or .devbox/) turns "I forgot to allow direnv" into an actionable message:
#!/usr/bin/env bash
set -euo pipefail
case "$(command -v terraform)" in
  /nix/store/*|*/.devbox/*) echo "project toolchain active" ;;
  *) echo "project toolchain not loaded: install the direnv hook and run 'direnv allow'"; exit 1 ;;
esac
  1. Review .envrc changes like code. Because every developer runs it, a CODEOWNERS entry for .envrc is reasonable. Keep it short so reviews are easy.

  2. Never put secrets in .envrc. It is committed. Load secrets from a git-ignored file with dotenv_if_exists, or from a secret manager as described in pulling dev secrets from 1Password at startup.

Time to Enter the Project Directory Bar chart comparing directory-entry time with plain use flake and with nix-direnv caching. Time to Enter the Project Directory use flake, no cache 3.4 s nix-direnv cached 45 ms
Measured on a polyglot flake; caching turns every cd into a near no-op.

Platform caveats

macOS: the default login shell is zsh; the hook goes in ~/.zshrc. Terminal apps that start login shells read ~/.zprofile first, which is fine — the hook in ~/.zshrc still runs for interactive shells.

WSL2: install direnv inside the distribution. VS Code's Remote WSL extension runs the direnv extension on the Linux side, so install it in the WSL context, not locally on Windows.

Apple Silicon (ARM64): nothing architecture-specific in direnv; flake evaluation uses the native aarch64-darwin outputs.

Rollback

Remove the hook line from the shell rc file and delete .envrc; the environment simply stops loading automatically, and nix develop or devbox shell still work by hand:

#!/usr/bin/env bash
set -euo pipefail
sed -i.bak '/direnv hook/d' ~/.zshrc
git rm --cached .envrc 2>/dev/null || true
rm -f .envrc

Frequently Asked Questions

Why do I have to run direnv allow again after pulling?

direnv records a hash of each allowed .envrc. When a pull changes the file, the hash no longer matches and direnv blocks it until you review and allow the new version. This prevents a malicious or mistaken change from executing silently.

Is use flake slow?

Without caching it re-evaluates the flake whenever the directory changes, which can take seconds. With nix-direnv installed, the evaluated environment is cached and reused until flake.nix or flake.lock changes.

Can I have personal environment variables without changing the shared .envrc?

Yes. Put them in a git-ignored .env.local and load it with dotenv_if_exists .env.local, or create .envrc.local and add source_env_if_exists .envrc.local to the shared file.

Does direnv work in CI?

It can, with direnv exec . <command>, but CI is simpler with nix develop --command or devbox run directly. direnv is primarily an interactive convenience.