以下内容是我跟codex共同编辑的。
前情提要
佬们是否有遇到过在WSL里启动Codex,它调用命令查询你Win侧一些命令不能很容易的发现,然而你手动在powershell里执行却不会报错?这不只是没加.exe的问题,因为在WSL里启动cmd/powershell来查询Win侧命令,本就不应该出现问题。
经过codex的头脑风暴,顺着以下路子找到了解决方法。
先总结一下:是Codex插件的问题
![]()
一次离奇的 PATHEXT=.CPL 排查记录:WSL、PowerShell、Codex VS Code 扩展和 WSLENV
最近遇到一个很迷惑的问题:在 Windows + WSL 环境里,从 Codex/VS Code 扩展的 WSL 会话启动 Windows 侧 pwsh.exe 时,PowerShell 里的 PATHEXT 居然只剩:
.CPL
这会导致 PowerShell 里查找 Windows 命令异常。比如 nssm 明明装好了,cmd.exe where nssm 能找到,但在这个特殊环境里:
Get-Command nssm
找不到。必须写成 nssm.exe 才行。
正常环境是什么样
先在普通 Windows PowerShell 里看:
$env:PATHEXT
[Environment]::GetEnvironmentVariable('PATHEXT', 'Process')
[Environment]::GetEnvironmentVariable('PATHEXT', 'Machine')
[Environment]::GetEnvironmentVariable('PATHEXT', 'User')
正常 Process 值是:
.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW;.CPL
Machine 里少一个 .CPL:
.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW
这本身没问题,PowerShell 进程里补上 .CPL 也正常。
普通 Ubuntu/WSL 里也没问题:
printf 'WSLENV=<%s>\nPATHEXT=<%s>\n' "$WSLENV" "$PATHEXT"
pwsh.exe -NoLogo -NoProfile -Command '$env:PATHEXT'
输出大概是:
WSLENV=<>
PATHEXT=<>
.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW;.CPL
也就是说,Linux 侧没有 PATHEXT 很正常;从普通 WSL 启动 Windows pwsh.exe 时,Windows 子进程会自然拿到 Windows 侧正确的 PATHEXT。
异常只出现在 Codex/VS Code 扩展启动的 WSL 会话
在 Codex 会话里查:
printf 'WSLENV=<%s>\nPATHEXT=<%s>\n' "$WSLENV" "$PATHEXT"
pwsh.exe -NoLogo -NoProfile -NonInteractive -Command '$env:PATHEXT'
最初看到的是:
WSLENV=<PATHEXT/l:COMSPEC/p:SYSTEMROOT/p:...>
PATHEXT=<>
.CPL
关键线索是:
PATHEXT/l
/l 的坑

WSLENV 的 /l 是“路径列表”语义,不是“任意列表”。
测试:
cmd.exe /d /c "set FOO=.COM;.EXE;.BAT;.CMD&& set WSLENV=FOO&& wsl.exe -e printenv FOO"
能得到:
.COM;.EXE;.BAT;.CMD
但:
cmd.exe /d /c "set FOO=.COM;.EXE;.BAT;.CMD&& set WSLENV=FOO/l&& wsl.exe -e printenv FOO"
结果变量直接没了。
而真正的路径列表可以正常转换:
cmd.exe /d /c "set FOO=C:\Windows;C:\Users&& set WSLENV=FOO/l&& wsl.exe -e printenv FOO"
输出:
/mnt/c/Windows:/mnt/c/Users
所以问题很清楚:PATHEXT 虽然是一个“列表”,但它不是“路径列表”。它是 Windows 可执行扩展名列表,不能用 /l。
进一步定位到 VS Code 扩展
继续查当前进程链:
ps -o pid,ppid,comm,args -p $$ -p $PPID
tr '\0' '\n' </proc/$PPID/environ | rg '^(WSLENV|PATHEXT)='
发现 Codex 会话的父进程是 VS Code 扩展里的 Codex app-server:
/mnt/c/Users/xxx/.vscode/extensions/openai.chatgpt-26.5429.30905-win32-x64/bin/linux-x86_64/codex app-server --analytics-default-enabled
然后在扩展文件里搜:
rg -n --hidden --no-ignore "PATHEXT/l|WSLENV|PATHEXT" \
/mnt/c/Users/xxx/.vscode/extensions/openai.chatgpt-26.5429.30905-win32-x64
在打包后的 JS 里发现了关键逻辑:
$De=[{name:"PATHEXT",type:"list"},{name:"COMSPEC",type:"path"},{name:"SYSTEMROOT",type:"path"},...]
这个 type:"list" 会生成:
PATHEXT/l
于是根因就是:OpenAI/Codex VS Code 扩展把 PATHEXT 当成了 WSLENV path list 传递。
为什么不是改成 "PATHEXT"
一开始想把:
{name:"PATHEXT",type:"list"}
改成:
"PATHEXT"
但进一步验证后发现,更干净的做法是:完全不要通过 WSLENV 传 PATHEXT。
因为普通 WSL 里没有 PATHEXT,也没有 WSLENV 时,从 WSL 启动 Windows pwsh.exe 反而能自然得到完整值。
也就是说,不传 PATHEXT 不是禁用它,而是避免 WSLENV 覆盖/干扰 Windows 子进程自己的正常环境。
临时修复:patch 扩展文件
修改这个文件:
C:\Users\xxx\.vscode\extensions\openai.chatgpt-26.5429.30905-win32-x64\out\extension.js
WSL 路径:
/mnt/c/Users/xxx/.vscode/extensions/openai.chatgpt-26.5429.30905-win32-x64/out/extension.js
删除这一段:
{name:"PATHEXT",type:"list"},
删除后列表从:
$De=[{name:"PATHEXT",type:"list"},{name:"COMSPEC",type:"path"},...
变成:
$De=[{name:"COMSPEC",type:"path"},...
验证:
perl -0777 -ne '$c=()=/{name:"PATHEXT",type:"list"},/g; print "PATHEXT-list-occurrences=$c\n"' \
/mnt/c/Users/xxx/.vscode/extensions/openai.chatgpt-26.5429.30905-win32-x64/out/extension.js
node --check /mnt/c/Users/xxx/.vscode/extensions/openai.chatgpt-26.5429.30905-win32-x64/out/extension.js
结果:
PATHEXT-list-occurrences=0
node --check 通过
重启 VS Code/Codex 后,再测:
printf 'WSLENV=<%s>\nPATHEXT=<%s>\n' "$WSLENV" "$PATHEXT"
pwsh.exe -NoLogo -NoProfile -NonInteractive -Command '$env:PATHEXT'
pwsh.exe -NoLogo -NoProfile -NonInteractive -Command 'Get-Command nssm -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source'
修复后:
WSLENV=<COMSPEC/p:SYSTEMROOT/p:...>
PATHEXT=<>
.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW;.CPL
C:\Users\xxx\AppData\Local\Microsoft\WinGet\Packages\NSSM.NSSM_Microsoft.Winget.Source_8wekyb3d8bbwe\nssm-2.24-101-g897c7ad\win64\nssm.exe
成功。
长效保护:在 WSL 的 .profile 里清掉错误项
扩展更新后,extension.js 的本地修改可能会被覆盖。所以又加了一层长效保护,放在:
/home/xxx/.profile
加入:
# Work around the OpenAI/Codex VS Code extension passing PATHEXT via WSLENV.
# PATHEXT is a Windows executable-extension list, not a WSL path/list value;
# carrying it through WSLENV can break Windows command lookup from WSL.
if [ -n "${WSLENV:-}" ]; then
_codex_wslenv_new=
_codex_wslenv_old_ifs=$IFS
IFS=:
for _codex_wslenv_entry in $WSLENV; do
case "$_codex_wslenv_entry" in
PATHEXT|PATHEXT/*)
continue
;;
esac
_codex_wslenv_new="${_codex_wslenv_new:+$_codex_wslenv_new:}$_codex_wslenv_entry"
done
IFS=$_codex_wslenv_old_ifs
export WSLENV=$_codex_wslenv_new
unset _codex_wslenv_new _codex_wslenv_old_ifs _codex_wslenv_entry
fi
.profile 是 WSL 用户的 login shell 启动脚本。Codex 扩展启动 WSL 里的 Codex 时用了类似:
/usr/bin/bash -lc ...
这里的 -l 会读取 .profile,所以这段保护会在 Codex 真正运行前生效。
验证:
bash -n ~/.profile
模拟扩展又带回错误值:
env -u PATHEXT WSLENV='PATHEXT/l:COMSPEC/p:SYSTEMROOT/p:SYSTEMDRIVE:USERNAME' \
bash -lc 'printf "WSLENV=<%s>\nPATHEXT=<%s>\n" "$WSLENV" "$PATHEXT"; pwsh.exe -NoLogo -NoProfile -NonInteractive -Command "$env:PATHEXT"'
结果里 PATHEXT/l 被移除了,pwsh.exe 里的 PATHEXT 也正常。
可能副作用
这个 .profile 保护逻辑会移除 WSLENV 里的:
PATHEXT
PATHEXT/...
对日常使用基本是正向的,因为 PATHEXT 不应该通过 WSLENV 传。普通 WSL 不传 PATHEXT 时,Windows 子进程自己能拿到正确值。
理论副作用是:如果某个工具非常特殊,故意想通过 WSLENV=PATHEXT... 从 WSL 向 Windows 子进程传一个自定义 PATHEXT,这段逻辑会拦掉它。不过这个场景很少见。
总结
这次问题的关键不是 Windows 配置坏了,也不是 WSL 坏了,更不是 PowerShell 坏了。
真正的问题是:
Codex VS Code 扩展把 PATHEXT 当成 WSLENV 路径列表传递了
错误形式:
PATHEXT/l
正确处理:
不要通过 WSLENV 传 PATHEXT
临时修扩展,长效在 .profile 里做防护。修完以后,pwsh.exe 的 PATHEXT 正常,Get-Command nssm 也正常。
1 个帖子 - 1 位参与者