claude-code - 💡(How to fix) Fix Shell snapshot overrides `.zshenv`, breaking cwd-aware shell setup

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Claude Code's per-session shell snapshot (~/.claude/shell-snapshots/snapshot-zsh-*.sh) captures export PATH=... from the parent shell at launch time, then sources that snapshot in every Bash tool invocation. This silently overrides any PATH adjustment made by ~/.zshenv, breaking cwd-aware setup like nvm auto-switching for non-interactive subshells.

The result: shell-level configuration cannot reliably control the environment of Bash tool calls. Users who want per-project Node/Python/etc. versions in Bash tool subshells are forced into per-command PreToolUse hooks that prepend setup to every command, which in turn pollutes the command string seen by the permissions allowlist.

Error Message

The Bash tool spawns:

Root Cause

This works but has a serious side effect: the rewritten command is what the permissions matcher sees. Allowlist rules like Bash(npm run lint) no longer match because the actual command is now export NVM_DIR=...; nvm use --silent...; npm run lint. Users either lose auto-permissions entirely or must contort their allowlist with prefix patterns that will rot.

Fix Action

Fix / Workaround

Common workaround is a PreToolUse Bash hook that prepends nvm sourcing to the command:

Neither workaround is correct:

  1. Source .zshenv after the snapshot, or run a user-controlled rcfile after the snapshot. A small "post-snapshot user hook" file (e.g., ~/.claude/shell-postinit.sh) that gets sourced at the end of the wrapper, after the snapshot but before eval, would let users do nvm use (or equivalent) per invocation without rewriting commands.
  2. Don't bake PATH into the snapshot at launch time. Capture functions, aliases, options, and exported scalars, but let the live shell's startup files determine PATH. This is the more invasive fix but the most correct: snapshots should restore the user's configuration, not freeze a stale environment that supersedes that configuration.
  3. Have PreToolUse hooks run AFTER the permission check, not before. Then the existing per-command nvm prefix hook would be invisible to the matcher and the workaround would be acceptable. Schema already has if: "Bash(...)" filtering, but ordering is the real issue.
  4. Document the snapshot mechanism and its precedence. Right now there's no indication in user-facing docs that .zshenv will lose to a captured PATH. At minimum, this should be called out so people don't waste time chasing it.

Code Example

/bin/zsh -c "source <snapshot.sh> 2>/dev/null || true && <env exports> && setopt NO_EXTENDED_GLOB 2>/dev/null || true && eval '<user command>' < /dev/null && pwd -P >| /tmp/claude-ccfa-cwd"

---

{
  "type": "command",
  "command": "jq -c '.tool_input.command = (\"export NVM_DIR=...; nvm use --silent...; \" + (.tool_input.command // \"\")) | ...'"
}

---

# 1. Have nvm + .zshrc with the standard auto-switch hook on chpwd.
# 2. Have a project with .nvmrc pinning a non-default Node version.
# 3. Create ~/.zshenv:
cat > ~/.zshenv <<'EOF'
if [ -n "$CLAUDECODE" ]; then
  export NVM_DIR="$HOME/.nvm"
  [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" --no-use >/dev/null 2>&1
  [ -f .nvmrc ] && nvm use --silent >/dev/null 2>&1
fi
EOF
# 4. Open Claude Code in the project from a shell whose default Node differs
#    from .nvmrc.
# 5. Ask Claude to run `node --version`.
# Expected: the .nvmrc version.
# Actual: the launch-time default.
RAW_BUFFERClick to expand / collapse

Shell snapshot overrides .zshenv, breaking cwd-aware shell setup

Summary

Claude Code's per-session shell snapshot (~/.claude/shell-snapshots/snapshot-zsh-*.sh) captures export PATH=... from the parent shell at launch time, then sources that snapshot in every Bash tool invocation. This silently overrides any PATH adjustment made by ~/.zshenv, breaking cwd-aware setup like nvm auto-switching for non-interactive subshells.

The result: shell-level configuration cannot reliably control the environment of Bash tool calls. Users who want per-project Node/Python/etc. versions in Bash tool subshells are forced into per-command PreToolUse hooks that prepend setup to every command, which in turn pollutes the command string seen by the permissions allowlist.

Environment

  • macOS 25.3.0 (Darwin)
  • Claude Code 2.1.132 (VS Code extension)
  • $SHELL=/bin/zsh, zsh 5.9
  • nvm with multiple Node versions installed; per-project .nvmrc pinning

Observed behavior

The Bash tool spawns:

/bin/zsh -c "source <snapshot.sh> 2>/dev/null || true && <env exports> && setopt NO_EXTENDED_GLOB 2>/dev/null || true && eval '<user command>' < /dev/null && pwd -P >| /tmp/claude-ccfa-cwd"

Order of events inside that single invocation:

  1. zsh starts, sources ~/.zshenv (e.g., loads nvm, runs nvm use against the current .nvmrc, prepends the right Node bin to PATH).
  2. The -c body runs and sources the snapshot, which contains a verbatim export PATH=...v22.11.0... captured at launch.
  3. The snapshot's PATH overwrites whatever .zshenv set.
  4. eval '<user command>' runs with the launch-time PATH, not the cwd-aware one.

Confirmed by tracing: a sentinel echo ... >> /tmp/zshenv-trace in .zshenv does fire, then which node after the eval still resolves to the snapshot-captured version, not the .nvmrc version.

Why it matters

Common workaround is a PreToolUse Bash hook that prepends nvm sourcing to the command:

{
  "type": "command",
  "command": "jq -c '.tool_input.command = (\"export NVM_DIR=...; nvm use --silent...; \" + (.tool_input.command // \"\")) | ...'"
}

This works but has a serious side effect: the rewritten command is what the permissions matcher sees. Allowlist rules like Bash(npm run lint) no longer match because the actual command is now export NVM_DIR=...; nvm use --silent...; npm run lint. Users either lose auto-permissions entirely or must contort their allowlist with prefix patterns that will rot.

Neither workaround is correct:

  • Pre-command hook → pollutes permission matching.
  • ~/.zshenv → loses to the snapshot.
  • .zshrc → only sourced for interactive shells, never for zsh -c.
  • chpwd hook → only fires on cd, not at shell startup, and is in .zshrc anyway.
  • precmd/preexec → don't fire in zsh -c non-interactive mode.

There is currently no first-class way to have cwd-dependent environment in Bash tool subshells.

Suggested fixes (any one would help)

  1. Source .zshenv after the snapshot, or run a user-controlled rcfile after the snapshot. A small "post-snapshot user hook" file (e.g., ~/.claude/shell-postinit.sh) that gets sourced at the end of the wrapper, after the snapshot but before eval, would let users do nvm use (or equivalent) per invocation without rewriting commands.
  2. Don't bake PATH into the snapshot at launch time. Capture functions, aliases, options, and exported scalars, but let the live shell's startup files determine PATH. This is the more invasive fix but the most correct: snapshots should restore the user's configuration, not freeze a stale environment that supersedes that configuration.
  3. Have PreToolUse hooks run AFTER the permission check, not before. Then the existing per-command nvm prefix hook would be invisible to the matcher and the workaround would be acceptable. Schema already has if: "Bash(...)" filtering, but ordering is the real issue.
  4. Document the snapshot mechanism and its precedence. Right now there's no indication in user-facing docs that .zshenv will lose to a captured PATH. At minimum, this should be called out so people don't waste time chasing it.

Repro

# 1. Have nvm + .zshrc with the standard auto-switch hook on chpwd.
# 2. Have a project with .nvmrc pinning a non-default Node version.
# 3. Create ~/.zshenv:
cat > ~/.zshenv <<'EOF'
if [ -n "$CLAUDECODE" ]; then
  export NVM_DIR="$HOME/.nvm"
  [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" --no-use >/dev/null 2>&1
  [ -f .nvmrc ] && nvm use --silent >/dev/null 2>&1
fi
EOF
# 4. Open Claude Code in the project from a shell whose default Node differs
#    from .nvmrc.
# 5. Ask Claude to run `node --version`.
# Expected: the .nvmrc version.
# Actual: the launch-time default.

Happy to provide trace output or test alternate fixes if useful.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING