[BUG] Windows: Bash tool commands are silently truncated at ~8,181 chars of the bash.exe -c argument, and every \\ is halved
What happens
On Windows the Bash tool passes the whole command to Git's bash.exe as a single -c argument:
bash.exe -c "source <snapshot> && export TEMP=... && eval '<command, each ' as '"'"'>' && pwd -P >| <cwdfile>"
(That's /proc/$$/cmdline from inside the tool's shell.) Two things go wrong at the node → bash.exe argv hand-off, and I can reproduce both with plain Node spawning bash.exe directly, so this isn't the wrapper — it's the transport:
- The
-cargument is cut off between 8,181 and 8,190 characters. Bash then reportsunexpected EOF while looking for matching'`(orhere-document ... delimited by end-of-file) and nothing runs. Because the wrapper rewrites every'as'"'"', the line number in the error is just the last'in the user's text before the cut, which sends you hunting for a quoting bug that doesn't exist. libuv only refuses at 32,767 (ENAMETOOLONG`), so anything between ~7.5K and 32K chars fails this way with no diagnostic pointing at length. - Every doubled backslash arrives halved.
A\\Breaches bash asA\B,G\\\\HasG\\H— inside a quoted heredoc too. libuv quotes the argument by MS-CRT rules (a backslash is only doubled before a"), while the MSYS2 runtime parses\\inside a double-quoted argument as an escape. Writing the same script to a file and running the file is byte-exact.
A census of my own transcripts (7,815 Bash commands, 37 sessions) shows 0 truncation failures under 8K expanded chars and 25 of 29 at 9K and above; plus a handful of SyntaxError: unterminated string literal in Python heredocs that were the halving.
Repro (no Claude Code needed)
// node repro.js -- Node 24.19, Git for Windows 2.55.0.windows.3
const { spawnSync } = require('node:child_process');
const bash = 'C:\\Program Files\\Git\\usr\\bin\\bash.exe'; // same result with bin\bash.exe
for (const n of [8100, 8180, 8190, 8200, 9000]) {
const script = 'echo START; : ' + 'a'.repeat(n - 24) + '; echo END';
const r = spawnSync(bash, ['-c', script], { encoding: 'utf8' });
console.log(script.length, JSON.stringify(r.stdout.trim()), r.stderr.trim().slice(0, 60));
}
// 8100 "START\nEND"
// 8180 "START\nEND"
// 8190 "START" <- tail gone, no error
// 8200 "START"
// 9000 "START"
const r = spawnSync(bash, ['-c', "printf '%s\\n' 'A\\\\B G\\\\\\\\H' | /usr/bin/od -c"], { encoding: 'utf8' });
console.log(r.stdout); // A \ B G \ \ H <- sent A\\B G\\\\H
Through the Bash tool the ceiling for the user's command is lower — roughly 7,000 chars / 100 lines — because the wrapper text and the '"'"' expansion share the same 8K budget.
Expected
Either the command runs, or the tool says it's too long. Today it does neither: the command is silently truncated and bash reports a quoting error inside the body.
Suggested fix
On Windows, hand the script to bash via stdin or a temp file (bash <file> / bash -s < file) instead of as a -c argument — that removes both the length cap and the backslash re-interpretation. Failing that, refuse commands whose assembled -c argument exceeds ~8,100 chars with a clear message.
Environment
- Claude Code 2.1.263, Windows 11 Enterprise 10.0.26200
- Node v24.19.0 (nvm4w), Git for Windows 2.55.0.windows.3 (
bin\bash.exewrapper →usr\bin\bash.exe, MSYS2 bash 5.3.15) - Same behaviour with
CLAUDE_CODE_GIT_BASH_PATHunset (default discovery)