背景

我在 Windows 上使用 Codex Coding Agent、PowerShell 7 和 mise 管理开发运行时。mise 的配置、缓存和安装目录统一放在 D:\XDG 下:

XDG_CONFIG_HOME = D:\XDG\.config
XDG_DATA_HOME   = D:\XDG\.local\share
XDG_CACHE_HOME  = D:\XDG\.cache

正常情况下,mise doctor 应该显示:

config: D:\XDG\.config\mise
data:   D:\XDG\.local\share\mise
shims:  D:\XDG\.local\share\mise\shims

但在 Codex Coding Agent 中,某些命令会调用 D:\XDG\.local\share\mise\shims\node.exe,随后生成大量 node.exemise.exe 子进程,最终把内存拖死。

现象

在 Codex 环境里运行 node --version 后,进程树中出现了这样的递归链:

node.exe "D:\XDG\.local\share\mise\shims\node.exe" --version
  -> mise.exe "mise" x -- node --version
    -> node.exe "D:\XDG\.local\share\mise\shims\node.exe" --version
      -> mise.exe "mise" x -- node --version
      ...

这个链会持续扩张。一次复现中,短时间内生成了数百个 node.exe / mise.exe 进程。

为了止血,我先临时禁用了 Node shim:

Rename-Item `
  'D:\XDG\.local\share\mise\shims\node.exe' `
  'node.exe.disabled-by-codex-rootcause'

禁用 shim 后,再清理残留 node.exe / mise.exe 进程,递归才停止。

初步误判

一开始我怀疑是 Codex 从旧的 GUI 父进程继承了过期环境变量。因为 Codex 的父进程链是:

Microsoft.CmdPal.UI.exe
  -> Codex.exe
    -> codex.exe app-server
      -> pwsh.exe

Windows 进程的环境变量在进程创建时固定,后续修改系统环境变量不会自动同步到已运行的 GUI 进程。这个判断有一定合理性。

但注销、重启过,问题依然存在。因此根因不应继续归因于“旧进程没有刷新环境”。

关键差异

在 Windows Terminal 的 PowerShell 中,mise doctor 正常:

shims_on_path: yes
dirs:
  cache: D:\XDG\.cache\mise
  config: D:\XDG\.config\mise
  data: D:\XDG\.local\share\mise
  shims: D:\XDG\.local\share\mise\shims

config_files:
  D:\XDG\.config\mise\config.toml

toolset:
  core:node@26.4.0
  core:python@3.14.6
  aqua:pnpm/pnpm@11.10.0
  ...

但在 Codex 的 shell command 环境中,最初看到的是:

XDG_CONFIG_HOME = empty
XDG_DATA_HOME   = empty
XDG_CACHE_HOME  = empty

对应的 mise doctor 退回默认目录:

dirs:
  cache: ~\AppData\Local\Temp\mise
  config: ~\.config\mise
  data: ~\AppData\Local\mise
  shims: ~\AppData\Local\mise\shims

config_files:
  (none)

toolset:
  (empty)

但是 Codex 的 PATH 里仍然有:

D:\XDG\.local\share\mise\shims

这就形成了危险组合:

PATH 上有 D:\XDG 的 mise shims
但当前进程没有 XDG_* 环境变量
mise 自己退回 AppData 默认目录
toolset 为空
真实 install dirs 没有被正确插到 shim 前面

真正根因

进一步验证发现,注册表里的环境变量是存在的:

[Environment]::GetEnvironmentVariable('XDG_CONFIG_HOME', 'User')
[Environment]::GetEnvironmentVariable('XDG_CONFIG_HOME', 'Machine')

都能读到:

D:\XDG\.config

XDG_DATA_HOMEXDG_CACHE_HOME 也一样存在。

但是当前 Codex 启动的命令进程里,Process 作用域没有这些变量:

Process XDG_CONFIG_HOME = empty
Process XDG_DATA_HOME   = empty
Process XDG_CACHE_HOME  = empty

同一个 Codex 命令里,手动把注册表中的值注入当前进程:

$env:XDG_CONFIG_HOME = [Environment]::GetEnvironmentVariable('XDG_CONFIG_HOME', 'User')
$env:XDG_DATA_HOME   = [Environment]::GetEnvironmentVariable('XDG_DATA_HOME', 'User')
$env:XDG_CACHE_HOME  = [Environment]::GetEnvironmentVariable('XDG_CACHE_HOME', 'User')
mise doctor

mise doctor 立刻恢复正常,重新指向 D:\XDG

因此根因不是 mise 配置错误,也不是 Windows 环境变量没有设置,而是:

Codex 的命令执行环境不是完整继承 Windows 登录环境,而是由 Codex/app-server 构造过的进程环境块。它保留或重建了 PATH 和一部分常见变量,但漏掉了自定义的 XDG_* 环境变量。

同时也观察到 Codex 进程环境中缺少一些常见 Windows 变量,例如:

OS
NUMBER_OF_PROCESSORS
PROCESSOR_ARCHITECTURE
windir

这说明问题不是只针对 XDG_*,而是 runner 环境构造策略本身不是完整继承。

为什么会导致 shim 递归

正常的 mise 激活后,PATH 顺序应该类似:

D:\XDG\.local\share\mise\installs\node\26.4.0
D:\XDG\.local\share\mise\shims

也就是说,node 会先命中真实 Node:

D:\XDG\.local\share\mise\installs\node\26.4.0\node.exe

而不是 shim。

出问题时,Codex 环境里没有 XDG_*,mise 不知道应该使用 D:\XDG 那套配置和安装目录,于是没有把真实 install dirs 正确放到 shim 前面。但 PATH 上仍有:

D:\XDG\.local\share\mise\shims

因此 node 命中的是 shim:

D:\XDG\.local\share\mise\shims\node.exe

shim 又调用:

mise x -- node --version

mise x 内部再次解析 node 时,仍然从 PATH 命中同一个 shim。于是形成:

node shim -> mise x -> node shim -> mise x -> ...

这就是进程爆炸的直接原因。

修复方案

最终采用的修复是:在 PowerShell profile 中,在 mise activate 之前,从 User/Machine 作用域补回当前进程缺失的 XDG 变量。

放在这一行之前:

(&mise activate pwsh) | Out-String | Invoke-Expression

补丁如下:

foreach ($name in @('XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME')) {
    if (-not [Environment]::GetEnvironmentVariable($name, 'Process')) {
        $value = [Environment]::GetEnvironmentVariable($name, 'User')
        if (-not $value) {
            $value = [Environment]::GetEnvironmentVariable($name, 'Machine')
        }
        if ($value) {
            [Environment]::SetEnvironmentVariable($name, $value, 'Process')
        }
    }
}

这样即使 Codex 启动 shell 时没有传入 XDG_*,PowerShell profile 也会在 mise 初始化前补齐。

修复后,Codex 中重新验证:

XDG_CONFIG_HOME = D:\XDG\.config
XDG_DATA_HOME   = D:\XDG\.local\share
XDG_CACHE_HOME  = D:\XDG\.cache

mise doctor 恢复为:

shims_on_path: yes
dirs:
  cache: D:\XDG\.cache\mise
  config: D:\XDG\.config\mise
  data: D:\XDG\.local\share\mise
  shims: D:\XDG\.local\share\mise\shims

config_files:
  D:\XDG\.config\mise\config.toml

toolset:
  core:node@26.4.0
  core:python@3.14.6
  ...

并且 Get-Command node 解析到真实安装目录:

D:\XDG\.local\share\mise\installs\node\26.4.0\node.exe

这说明递归条件已经消失。

恢复 shim

在确认 Codex 里的 mise doctor 已经指向 D:\XDG 后,可以恢复之前临时禁用的 Node shim:

Rename-Item `
  'D:\XDG\.local\share\mise\shims\node.exe.disabled-by-codex-rootcause' `
  'node.exe'

mise reshim

然后验证:

mise doctor
Get-Command node -All
node --version

期望结果:

mise doctor 不再报告 Missing shims: node.exe
node --version 输出 v26.4.0
没有新的 node.exe / mise.exe 递归进程

定位思路总结

这类问题不要只看“在我自己的终端里是否正常”,而要看失败进程实际看到的环境。

排查顺序可以是:

  1. 看命令解析结果:
Get-Command node -All
Get-Command mise -All
  1. 看当前进程环境:
$env:XDG_CONFIG_HOME
$env:XDG_DATA_HOME
$env:XDG_CACHE_HOME
$env:PATH
  1. 看注册表环境:
[Environment]::GetEnvironmentVariable('XDG_CONFIG_HOME', 'User')
[Environment]::GetEnvironmentVariable('XDG_CONFIG_HOME', 'Machine')
  1. 看 mise 实际使用的目录:
mise doctor
  1. 看是否存在递归进程链:
Get-CimInstance Win32_Process |
  Where-Object { $_.Name -match 'mise|node' } |
  Select-Object ProcessId, ParentProcessId, Name, CommandLine

如果看到:

node shim -> mise x -- node -> node shim

就说明已经不是普通 PATH 错误,而是 shim 递归。

预防建议

1. 不要只依赖 GUI 应用完整继承环境变量

GUI 应用、后台服务、Electron 应用、Agent runner 都可能重建环境变量。只要依赖自定义环境变量,就应该在 shell 初始化阶段做兜底。

对 mise 这种强依赖 XDG_* 的配置,建议在 profile 中显式补齐关键变量。

2. mise activate 必须发生在 XDG 变量之后

顺序很重要:

# 先补 XDG
# 再 mise activate
(&mise activate pwsh) | Out-String | Invoke-Expression

如果先 activate,mise 已经按错误目录完成初始化,后面再补变量也来不及。

3. shims 不应优先于真实 install dirs

正常激活后,真实 install dirs 应该排在 shim 前面。可用下面的方式检查:

($env:PATH -split ';') | Select-String 'mise'
Get-Command node -All

理想结果是 node 首先解析到:

D:\XDG\.local\share\mise\installs\node\...\node.exe

而不是:

D:\XDG\.local\share\mise\shims\node.exe

4. 对 Agent 环境做启动自检

可以在 Agent 工作流开始时检查:

if (-not $env:XDG_CONFIG_HOME -or -not $env:XDG_DATA_HOME -or -not $env:XDG_CACHE_HOME) {
    throw "XDG environment variables are missing in this process."
}

mise doctor

如果 mise doctor 显示 ~\AppData\Local\mise,就不要继续运行 nodepythonpnpm 这类 shim 命令。

5. 出现递归时优先止血

如果已经发生进程爆炸,先阻断递归入口,再清进程。

对这次问题,阻断入口是临时禁用 node.exe shim:

Rename-Item `
  'D:\XDG\.local\share\mise\shims\node.exe' `
  'node.exe.disabled-by-codex-rootcause'

然后再清理残留进程。否则单纯杀进程可能追不上新的子进程派生。

结论

这次问题的表面现象是 Codex 调用 mise shim 导致 node.exe / mise.exe 无限递归;直接原因是 node 解析到了 shim,而 shim 内部再次通过 PATH 解析到自己。

更深层的根因是 Codex 的 tool runner 没有把 Windows 注册表中的自定义 XDG_* 环境变量带入命令进程,导致 mise 在 Codex 环境中退回默认 AppData 目录,而 PATH 上又保留了 D:\XDG 的 shims。

最终修复不是改 mise,也不是反复重启 Codex,而是在 PowerShell profile 中,在 mise activate 之前补齐 XDG_CONFIG_HOMEXDG_DATA_HOMEXDG_CACHE_HOME。这样可以让 Codex、普通终端和其它 PowerShell 启动场景都稳定使用同一套 mise 目录。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐