-
Notifications
You must be signed in to change notification settings - Fork 802
Claude Code
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.
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_COMMANDnever fires — there's no prompt in a non-interactive shell -
direnv reloadsignals 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.
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.
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 0Add 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_FILEis sourced in the current working directory before each Bash command -
direnv export bashevaluates the.envrcfor that directory and exports the variables - The
cdwrapper 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
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.
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)"
fiFor 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;;
esacLimitation: 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.
| Mechanism | Interactive Shell | Claude Code (non-interactive) |
|---|---|---|
direnv hook bash |
Sets up PROMPT_COMMAND — works |
Sets up PROMPT_COMMAND — never 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 |
-
Subprojects / monorepos: If your Bash command does
cd subfolder && command, thecdwrapper in Option 1 handles re-evaluation. Without it, the.envrcfrom the starting directory is used. -
direnv allow: You must have previously rundirenv allowfor each.envrc. The hook does not auto-allow — this is a security feature. -
Multiple
.envrcfiles:direnv export bashwalks up the directory tree, so nested.envrcfiles work as expected (child inherits from parent viasource_up). -
Performance:
direnv export bashruns on every Bash tool call. For most.envrcfiles this takes <50ms. If you have expensive setup (e.g., nix shells), consider caching.
-
direnv/direnv#262 — Running direnv from shell script — Maintainer confirms
direnv exportfor non-interactive use - anthropics/claude-code#2110 — direnv support feature request — Community workarounds and discussion
-
Claude Code Hooks Reference — SessionStart —
CLAUDE_ENV_FILEdocumentation -
Claude Code Settings — Bash tool behavior — How
CLAUDE_ENV_FILEis sourced before each command
NOTE: this wiki is community-maintained. Everyone is welcome to contribute their solution.