整了个Claude Code用,用来写个vscode扩展,让它跑一个pnpm测试命令,居然报错找不到pnpm。同样的命令手动在终端里跑完全没问题(显然)。
现象
要求Claude Code在它自己的Bash工具里跑which pnpm,exit code 1,找不到。但我自己在普通终端里跑是好的,返回正常路径。重启VSCode无效,重启系统(wsl2, 所以是wsl –shutdown)无效。
pnpm是通过Volta管理的,绝对路径可以正常执行,问题就是PATH里没有Volta的bin目录。
先试了最显然的方向
.bashrc里有标准的非交互式shell early return:
case $- in
*i*) ;;
*) return;;
esac
Volta的PATH设置在文件末尾,在early return之后。看起来很可疑,把它移到early return之前,重启,但是没用。
看来问题的根本原因不是这个(但这个也确实很重要。)
开始查进程链
我之前开启了Claude Code的bwrap sandbox特性,每次Bash工具调用都会在一个隔离的bwrap沙箱里执行。先看Bash工具自己跑在什么进程里:
cat /proc/$$/status | grep PPid
ps -p $(cat /proc/$$/status | awk '/PPid/{print $2}') -o pid,ppid,cmd
输出显示父进程是PID 1,而PID 1是一个bwrap命令。bwrap命令参数里有这么一行:
eval "source /home/USERNAME/.claude/shell-snapshots/snapshot-bash-xxxxxx.sh && ..."
原来Claude Code每次调用Bash工具,都是在bwrap沙箱里source一个snapshot文件,然后执行命令。Bash工具的环境完全由这个snapshot决定。
另外值得一提的是,sandbox还有一个已知bug:它会把项目目录下的.bashrc等dotfile绑定挂载到/dev/null,生成一批0字节只读占位文件。这和PATH问题是两个独立的问题,但确实让排查变得更混乱(相关issue:#17258、#17087)。
看一眼snapshot文件内容和PATH:
cat /home/USERNAME/.claude/shell-snapshots/snapshot-bash-xxxxxx.sh
grep "export PATH" /home/USERNAME/.claude/shell-snapshots/snapshot-bash-xxxxxx.sh
末尾有这么一行:
export PATH=/home/USERNAME/.local/share/uv/python/cpython-3.13.7-linux-x86_64-gnu/bin\:/home/USERNAME/.vscode-server/bin/.../bin/remote-cli\:/usr/local/sbin\:/usr/local/bin\:...
Volta的路径完全不在里面。而且这里有个奇怪的东西:uv的路径怎么进来的?为什么uv在里面但Volta不在?
这个路径在任何shell配置文件里都找不到,rg搜$HOME --max-depth=1,没有文件设置了这个,怪了,可能是别的东西带进来的环境变量吧。
顺着进程链找根源
用/proc/<PID>/environ查了所有相关进程的原始环境:
# 找到VSCode Server的进程
pgrep -f "vscode-server" | head -5
# 读取该进程启动时继承的原始环境,不经过任何shell处理
cat /proc/<PID>/environ | tr '\0' '\n' | grep PATH
# 顺着PPid往上查整条进程链
cat /proc/<PID>/status | grep PPid
for pid in 1190 1191 1197 1201 1247 1304 2566 2939; do echo "=== PID $pid: $(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | cut -c1-60) ==="; cat /proc/$pid/environ 2>/dev/null | tr '\0' '\n' | grep "^PATH"; done
=== PID 1190: sh -c "$VSCODE_WSL_EXT_LOCATION/scripts/wslServer.sh" bf9252 ===
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib
=== PID 1191: sh /mnt/c/Users/USERNAME/.vscode/extensions/ms-vscode-remote.rem ===
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib
=== PID 1197: sh /home/USERNAME/.vscode-server/bin/bf9252a2fb45be6893dd8870c0b ===
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib
=== PID 1201: /home/USERNAME/.vscode-server/bin/bf9252a2fb45be6893dd8870c0bf37 ===
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib
=== PID 1247: /home/USERNAME/.vscode-server/bin/bf9252a2fb45be6893dd8870c0bf37 ===
PATH=/home/USERNAME/.vscode-server/bin/bf9252a2fb45be6893dd8870c0bf37e2e1766d61/bin/remote-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib
=== PID 1304: /home/USERNAME/.vscode-server/bin/bf9252a2fb45be6893dd8870c0bf37 ===
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib
=== PID 2566: /home/USERNAME/.vscode-server/extensions/anthropic.claude-code-2 ===
PATH=/home/USERNAME/.local/share/uv/python/cpython-3.13.7-linux-x86_64-gnu/bin:/home/USERNAME/.vscode-server/bin/bf9252a2fb45be6893dd8870c0bf37e2e1766d61/bin/remote-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib
=== PID 2939: /home/USERNAME/.vscode-server/extensions/anthropic.claude-code-2 ===
PATH=/home/USERNAME/.local/share/uv/python/cpython-3.13.7-linux-x86_64-gnu/bin:/home/USERNAME/.vscode-server/bin/bf9252a2fb45be6893dd8870c0bf37e2e1766d61/bin/remote-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib
结论:
- WSL启动 → Windows侧拉起VSCode Server进程 → 进程链从头到尾从未经过任何用户shell init文件
- VSCode Server的直接父进程只带着系统级PATH,没有任何用户配置
uv的路径是Claude Code自己加进去的。
所以.bashrc改在哪都没用,snapshot生成时的起点环境就是残缺的。
问题的本质
Claude Code的snapshot机制设计初衷是主动捕获用户环境(2.15.1引入),但在WSL2 + VSCode Remote场景下,它生成snapshot时依赖的父进程环境本身就不完整,但Claude Code没有针对这个情况做额外的init文件处理。
SSH Remote是同样的问题,VSCode Server的启动方式决定了它不走用户shell init文件,不管是.bashrc还是.profile。
Workaround
VSCode Remote有一个文件~/.vscode-server/server-env-setup,官方文档里对它的描述是:在VSCode Server启动时执行,用于配置远端环境。正好我们可以拿来补充注入PATH:
# ~/.vscode-server/server-env-setup
. "$HOME/.bashrc"
注意这个文件得是sh兼容的。
这个文件执行后,环境变量会被后续所有进程继承,包括Claude Code生成snapshot时的shell。
.bashrc比.profile更合适,因为Volta的PATH已经在early return之前,source它会设置这些变量然后在early return处干净停止,不会执行交互式shell的部分。
结束
作为一个主打开箱即用的商业工具,既然发布了VSCode扩展,但却完全没考虑VSCode Remote的特殊性,作为一个商业工具我认为是不合理的。
相关issue
- #5202 — PATH/PYTHONPATH不被继承,closed as duplicate,原始issue closed as not planned
- #20503 — Bash工具文档与实际行为不一致(环境变量持久化)
- #32512 — VSCode扩展里环境变量不正确(macOS,近期)
- #17258 — sandbox在项目目录生成幽灵dotfile(
.bashrc等被挂载为空只读文件) - #17087 — 同上,另一个报告
- #1872 — shell snapshot机制(Oh My Zsh syntax error报告里有较详细的机制描述)
Last modified on 2026-03-14