Git for Windows 2.20.1(2018年版本)自带的 bin/bash.exe 存在 MSYS 参数编码 bug:通过 bash -c 传递包含 UTF-8 中文字符的命令时,参数内容会被破坏,导致所有中文命令报错 exit 127

解决方案:升级 Git for Windows 到最新版(2.55.0),问题消失。


起因

在使用 AI Agent(Hermes / Claude Code)的 terminal 工具时,发现一个诡异现象:任何包含中文的 shell 命令都会失败

$ echo hello
hello                          # 正常

$ echo 你好
exit code: 127                 # 失败

$ ls '/e/作业/'
exit code: 127                 # 失败

$ ls /e/作业/                   # 不加引号
(正常列出目录内容)             # 竟然成功了?

更奇怪的是,同样的命令在朋友电脑上完全正常


结论先行

经过排查,最终定位到根因:

项目 详情
根因 Git for Windows 2.20.1 的 bin/bash.exe 中,MSYS 运行时(msys-2.0.dll)将 Windows 命令行参数转换为 POSIX argv 时,破坏了 UTF-8 多字节字符
触发条件 bash -c '<含中文的参数>'
表现 参数中的换行符 \n 被转义为字面 \\n,中文字符被损坏
修复 升级 Git for Windows(2.20.1 → 2.55.0)

升级后验证:

$ git --version
git version 2.55.0.windows.2      # 从 2.20.1 升级

$ echo 你好
你好                               # 完美解决

$ ls -la '/e/作业/生产实习/'
total 880                           # 引号中文路径也没问题了

排查过程(比较长,可以直接跳过)

Step 1:控制变量 — 确认中文是触发条件

第一反应是排除噪音。设计了一组对照实验:

命令 结果 说明
echo hello OK 纯 ASCII
echo 你好 FAIL 127 加中文
pwd && ls -la OK 复合命令,无中文
ls '/e/作业/' FAIL 127 引号包裹中文路径
ls /e/作业/ OK 不加引号反而成功

结论明确:中文是必要触发条件

最后一个现象特别值得注意 —— 不加引号时中文路径能正常工作。这说明 bash 本身能处理中文,问题出在某个中间环节。

Step 2:分析错误输出 — 发现换行符丢失

echo 你好 的完整输出(这是 AI Agent 的 wrapper 脚本):

/usr/bin/bash: line 7: source .../hermes-snap-xxx.sh >/dev/null 2>&1 || true
builtin cd -- /c/Users/31548 || exit 126
eval 'echo 你好'
__hermes_ec=0
{ export -p > ... && mv -f ...; } 2>/dev/null || rm -f ... 2>/dev/null || true
pwd -P > .../hermes-cwd-xxx.txt 2>/dev/null || trueexit : No such file or directory

三个异常:

  1. wrapper 脚本内容泄露到了 stdout — 正常情况下不应该打印出来,说明 bash 解析脚本时出了问题
  2. trueexit 被合并成了 trueexit — 两行之间的换行符丢失了
  3. echo 你好 的输出没有出现 — 命令根本没执行到

这一步的关键收获:问题不在用户的命令,而在 bash 收到的脚本参数被破坏了

Step 3:读源码 — 理解执行链路

AI Agent 执行 shell 命令的链路是这样的:

# 1. 构造 wrapper 脚本(多行字符串)
script = "\n".join([
    "source snap.sh >/dev/null 2>&1 || true",
    "builtin cd -- /c/Users/31548 || exit 126",
    "eval 'echo 你好'",
    "__hermes_ec=$?",
    "exit $__hermes_ec"
])

# 2. 通过 bash -c 传递
subprocess.Popen(["D:/develop/Git/bin/bash.exe", "-c", script])

脚本在 Python 层面构造完全正确("\n".join() 不会丢失换行)。问题一定在 bash 收到参数之后。

Step 4:隔离测试 — 排除 Agent 本身的 bug

直接用 Python 调用 bash -c,不经过任何 Agent wrapper:

subprocess.run(['bash', '-c', 'echo line1\necho 你好\necho line3'])
# 结果: rc=0, stdout='line1\n你好\nline3'  **OK** 正常!

bash 本身没问题。 那问题在哪?

答案藏在细节里 —— 这里的 bash 走的是 /usr/bin/bash(MSYS 内部路径),不是 D:/develop/Git/bin/bash.exe(Windows 原生路径)。

Step 5:定位到具体 bash 可执行文件

系统上有多个 bash:

路径 来源
D:/develop/Git/bin/bash.exe Git for Windows 自带的 launcher
/usr/bin/bash MSYS 内部的 bash(同一个 Git 安装内)

AI Agent 通过 _find_bash() 查找 bash 时,优先使用了 D:/develop/Git/bin/bash.exe

这个文件是 Git for Windows 的 原生 Windows launcher,它的作用是:接收 Windows 命令行参数 → 调用 MSYS 运行时(msys-2.0.dll)转换 → 传给底层的 MSYS bash。

Step 6:对照实验 — 锁定 bug 位置

用同一个 Python 进程,分别调用两个 bash:

git_bash = "D:/develop/Git/bin/bash.exe"   # Windows launcher
msys_bash = "/usr/bin/bash"                 # MSYS bash

# Windows launcher + 中文
subprocess.run([git_bash, '-c', 'echo 你好'])
# → rc=127 **FAIL**

# MSYS bash + 中文(通过 launcher 调用)
subprocess.run([git_bash, '-c', '/usr/bin/bash -c "echo 你好"'])
# → rc=127 **FAIL**

# 直接在 MSYS 环境内调用
# (execute_code 沙箱内)
subprocess.run(['/usr/bin/bash', '-c', 'echo 你好'])
# → rc=0 **OK**

bug 在 Git/bin/bash.exe 的参数传递层,不在 bash 本身。

Step 7:找到铁证 — stderr 中的转义痕迹

subprocess.run([git_bash, '-c', 'echo 你好\necho world'], capture_output=True, text=True)

stderr 输出:

/usr/bin/bash: line 1: $'echo 你好\\necho world': command not found

$'...' 是 bash 的 ANSI-C 引用语法。注意 \\n —— 这不是换行符(0x0A),而是两个字符:反斜杠 + n

这意味着 msys-2.0.dll 在将 Windows 命令行转换为 POSIX argv 时,把 UTF-8 字节流搞乱了,连带把换行符也转义成了字面量。

Step 8:用 stdin 绕过验证

subprocess.run([git_bash], input='echo 你好\necho world', capture_output=True, text=True)
# → rc=0, stdout='你好\nworld' **OK**

通过 stdin 传递脚本,完全绕过了 -c 参数传递路径 → 正常。

最终确认: bug 存在于 Git/bin/bash.exe 的 MSYS 运行时将 Windows 命令行 -c 参数转换为 POSIX argv 的过程中,对 UTF-8 多字节字符的处理有误。


为什么无引号的中文路径能成功?

回到最初的困惑 —— ls /e/作业/ 能用,ls '/e/作业/' 不行。

原因是两条路径走的是不同的处理链路:

  • 有引号ls '/e/作业/' 整体作为 -c 参数的一部分传递 → 经过 msys-2.0.dll 参数转换 → 中文被破坏
  • 无引号ls /e/作业/ 中的 /e/作业/ 被 bash 直接作为文件路径处理 → 通过文件系统 API 访问 → 中文字节原样传递给 OS,不经过参数解析

完整证据链

用户输入 "echo 你好"
    │
Hermes 构造 wrapper 脚本 (Python 字符串, 换行正确)
    │
subprocess.Popen(["D:/develop/Git/bin/bash.exe", "-c", script])
    │
Python list2cmdline() 拼成 Windows 命令行字符串
    │
CreateProcess() 启动 bash.exe
    │
bash.exe → msys-2.0.dll 解析命令行 → 转换为 POSIX argv
    │  ← 此处 UTF-8 字节被破坏,\n 变成 \\n
    │
bash 收到损坏的 -c 参数
    │
bash 执行失败 → exit 127

排查方法论

1. 控制变量    → 确认"中文"是必要触发条件
2. 分析输出    → 发现 trueexit 合并 → 换行符丢失
3. 读源码      → 理解 wrapper 机制 → bash -c 传递
4. 隔离测试    → 绕过 Agent 直接调 bash → 正常 → 问题不在 bash 本身
5. 区分执行体  → 找到 Agent 用的 Git/bin/bash.exe
6. 对照实验    → 同一进程两个 bash,一好一坏 → 锁定 launcher
7. stdin 绕过  → 确认是 -c 参数传递路径的问题
8. 找铁证      → stderr 中 $'...'\\n' 暴露了 msys 转义 bug

核心思路:从现象出发,每一步排除一个可能性,逐步缩小嫌疑范围,直到只剩一个解释。 当遇到"同样的代码在别人电脑上能跑"的情况,优先怀疑环境差异(版本、路径、运行时)。


修复验证

项目 升级前 升级后
Git for Windows 2.20.1 (2018-12) 2.55.0 (2025)
bash 4.4.23 (pc-msys) 5.3.15 (pc-cygwin)
测试命令 升级前 升级后
echo hello OK OK
echo 你好 FAIL 127 OK
echo "中文测试" FAIL OK
ls '/e/作业/' FAIL 127 OK

升级 Git for Windows 后问题完全解决。

Logo

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

更多推荐