Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Drizzt321 edited this page Mar 8, 2026 · 2 revisions

Introduction

Claude Code is Anthropic's AI-powered coding assistant that runs commands through non-interactive shell sessions. This presents a challenge for direnv, which relies on PROMPT_COMMAND (bash) or precmd (zsh) — neither of which fires in non-interactive shells.

The Problem

direnv's shell hook (direnv hook bash/direnv hook zsh) works by registering a function that runs before each interactive prompt. Claude Code's Bash tool spawns non-interactive, non-login shells for each command, so:

  • .bashrc's default (at least in Debian) interactive guard (case $- in *i*) ;; *) return;; esac) bails before reaching direnv setup
  • Even without the guard, PROMPT_COMMAND never fires — there's no prompt in a non-interactive shell
  • direnv reload signals the hook to re-evaluate on next prompt, which never comes
  • Environment variables do not persist between Bash tool calls — each runs in a fresh shell

Claude Code sets CLAUDECODE=1 in its environment, which can be used to detect these sessions.

What does work

direnv export bash (a private command) directly evaluates the .envrc for the current directory and outputs export statements. This is the official recommendation for non-interactive shells from the direnv maintainer.

Solutions

Option 1: CLAUDE_ENV_FILE SessionStart Hook (Recommended)

Claude Code provides CLAUDE_ENV_FILE — a file that is sourced before every Bash tool command. A SessionStart hook can write direnv setup into this file at session start, giving you directory-aware environment loading on every command.

Create a hook script (e.g., ~/.claude/hooks/direnv-load.sh):

#!/bin/bash
# Writes direnv setup to CLAUDE_ENV_FILE, which is sourced before each Bash command.
# Also wraps cd so mid-command directory changes re-evaluate direnv.

if [ -n "$CLAUDE_ENV_FILE" ]; then
  cat >> "$CLAUDE_ENV_FILE" <<'DIRENV'
eval "$(direnv export bash)"
cd() {
  builtin cd "$@" && eval "$(direnv export bash)"
}
DIRENV
fi
exit 0

Add it to your Claude Code settings (~/.claude/settings.json or .claude/settings.json):

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/direnv-load.sh"
          }
        ]
      }
    ]
  }
}

Why this works:

  • CLAUDE_ENV_FILE is sourced in the current working directory before each Bash command
  • direnv export bash evaluates the .envrc for that directory and exports the variables
  • The cd wrapper ensures that if a single command changes directories (e.g., cd subproject && make), direnv re-evaluates for the new directory
  • Variables like git identity, PATH modifications, API keys, and virtual environments are all picked up correctly

Option 2: CLAUDE_ENV_FILE via Environment Variable

Set CLAUDE_ENV_FILE in your terminal before launching Claude Code, pointing to a persistent script:

Create ~/.config/direnv/claude-env.sh:

eval "$(direnv export bash)"
cd() {
  builtin cd "$@" && eval "$(direnv export bash)"
}

Then in your shell profile (before any interactive guard):

export CLAUDE_ENV_FILE="$HOME/.config/direnv/claude-env.sh"

Or add it to Claude Code's settings.json env block:

{
  "env": {
    "CLAUDE_ENV_FILE": "/path/to/claude-env.sh"
  }
}

This approach requires no hooks but means the env file is always the same script. See Bash tool behavior for details.

Option 3: Shell Profile (Simple, Limited)

For zsh users, add to ~/.zshenv (which is sourced for all shell types):

if [[ -n "$CLAUDECODE" ]]; then
  eval "$(direnv hook zsh)"
  eval "$(direnv export zsh)"
fi

For bash users, add to ~/.bashrc before the interactive guard:

if [[ -n "$CLAUDECODE" ]]; then
  eval "$(direnv export bash)"
fi

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

Limitation: This only loads the .envrc for the directory Claude Code starts in. It does not re-evaluate when changing directories, since PROMPT_COMMAND doesn't fire. For directory-aware loading, use Option 1 or 2.

How It All Fits Together

Mechanism Interactive Shell Claude Code (non-interactive)
direnv hook bash Sets up PROMPT_COMMAND — works Sets up PROMPT_COMMANDnever fires
direnv reload Signals hook to re-evaluate on next prompt No effect — no prompt cycle
direnv export bash Directly exports env diff — works Directly exports env diff — works
direnv exec DIR CMD Runs command with loaded env — works Runs command with loaded env — works
CLAUDE_ENV_FILE N/A Sourced before each Bash command — works

Edge Cases

  • Subprojects / monorepos: If your Bash command does cd subfolder && command, the cd wrapper in Option 1 handles re-evaluation. Without it, the .envrc from the starting directory is used.
  • direnv allow: You must have previously run direnv allow for each .envrc. The hook does not auto-allow — this is a security feature.
  • Multiple .envrc files: direnv export bash walks up the directory tree, so nested .envrc files work as expected (child inherits from parent via source_up).
  • Performance: direnv export bash runs on every Bash tool call. For most .envrc files this takes <50ms. If you have expensive setup (e.g., nix shells), consider caching.

References


NOTE: this wiki is community-maintained. Everyone is welcome to contribute their solution.

Clone this wiki locally

Morty Proxy This is a proxified and sanitized view of the page, visit original site.