[BUG] Windows: Bash commands >8KB silently truncated, backslash pairs silently collapsed (argv transport to MSYS2 bash)
Summary
On Windows, the Bash tool transports the whole command through argv to MSYS2'sbash -c. This causes two distinct failures:
- Commands over ~8,203 characters are silently truncated. Bash then reports a
bogus syntax error pointing at an arbitrary line in the middle of the content.
- **Doubled backslashes (
\\) are silently collapsed to a single one, at any
command size** (reproduced with a 295-byte command). No error is raised. The
file is simply written wrong.
Both disappear when the identical command is passed on bash's stdin (bash -s)
instead of as the -c argument. Verified byte-identical up to 259 KB.
The user-visible effect is that writing a file with a heredoc
(cat > file <<'EOF' ... EOF) fails once the content grows past ~8 KB, and can
silently corrupt backslashes even when it "succeeds".
Environment
- Windows 11 Pro 10.0.26200
- Git Bash 5.3.15(1)-release (x86_64-pc-cygwin)
- MSYS runtime: MINGW64_NT-10.0-26200 3.6.9-b4195d69.x86_64
- Process chain is
claude.exe->bash.exe(verified viaWin32_Process).
There is no cmd.exe involved, so the well-known 8,191-char cmd.exe limit
is not the explanation here, despite the similar magnitude.
Symptom 1: truncation reported as a syntax error
The error always looks like this, and it names a single quote, not a heredoc:
/usr/bin/bash: -c: line 102: unexpected EOF while looking for matching `''
The line it accuses is always a line in the middle of the payload that happens to
contain an apostrophe (won't, HDD's, $(date '+%Y-%m-%d')). That line is fine.
It is simply where the quote was left open after the cut.
Evidence the payload was valid. I extracted the 31 failing commands from my
session transcripts and re-fed the exact text to bash -n (syntax check, no
execution). 30 of 31 passed with no error. The text was correct; it never
arrived intact.
Evidence the cut is at a fixed position. The reported line number is
constant (line 108 in my synthetic repro) for command sizes from 8 KB through
24 KB. A content-dependent syntax problem would move; a fixed-offset truncation
does not.
Limits measured by bisection on this machine:
| component | limit |
|---|---|
| bash.exe -c (Git for Windows) | 8,203 chars |
| another MSYS binary (echo.exe), same runtime | received 20,000 intact |
| a native binary (python.exe) | received 32,000 intact |
| Windows CreateProcess | 32,767 documented for the whole command line |
So the bottleneck is specific to bash -c. Windows itself already allows far
more.
About that last row: 32,767 is the documented maximum for the entirelpCommandLine, including the module name and the terminating null. What I
actually measured is the largest surviving argument: 32,660 chars, failing at
32,661, with a 65-char executable path. The two figures are consistent, but only
the 32,660 is a measurement of mine.
Observed distribution across 711 session transcripts (1,188 heredoc commands):
| command size | succeeded | failed |
|---|---|---|
| under 4 KB | 1,069 | 0 |
| 4 KB to 8 KB | 77 | 0 |
| 8 KB to 32 KB | 0 | 31 |
| over ~32 KB | 0 | 5 (ENAMETOOLONG from uv_spawn) |
Method note on the first row: that zero is after excluding false positives from
this investigation itself. My own analysis scripts print the stringunexpected EOF in their stdout, and my transcript scanner counted that as a
failed command. I checked them case by case: all were from the investigation
session, and the count kept growing as I ran more analysis scripts. No
pre-investigation command under 4 KB failed.
Symptom 2: backslash pairs collapse, silently, at any size
This one is worse because nothing fails. Pairs of backslashes are halved; isolated
backslashes survive.
\\arrives as\\\\\arrives as\\- a Python regex
r'\\d+'arrives asr'\d+', which matches a digit
instead of a backslash. The script runs and does the wrong thing.
C:\Users\name(isolated backslashes) survives intact
Measured with a hostile payload (single quotes, double quotes, backslashes, $,
backticks, globs, accented characters) inside the real eval '...' wrapper:
| payload | via -c (current behavior) | via stdin |
|---|---|---|
| 295 B | corrupted (-4 B) | identical |
| 1 KB | corrupted (-16 B) | identical |
| 6.5 KB | corrupted (-100 B) | identical |
| 15 KB | not written | identical |
| 52 KB | WinError 206 | identical |
| 259 KB | WinError 206 | identical |
Note the first row: corruption at 295 bytes. This is not a size problem at all,
so chunking the payload does not mitigate it.
Root cause
Symptom 1 has a documented cause. It is the argv rebuild thatmsys2-runtime performs from the Win32 command line: build_argv() (indcrt0.cc) calls glob(), and it is glob() that uses a fixed
8192-character buffer allocated on the stack while doing Unicode conversion of
the pattern. From the upstream report:
"noglob is required, if the application would like to use command line arguments longer than 8192 characters and the command line includes any of the following characters:?*["'(){}(note that both single and double quote characters are included meaning thatglob ()code is typically hit for shell one-liners). This is becauseglob ()truncates the input, when making Unicode conversion for the pattern. For some reason (perhaps chicked and egg type problem), the buffer is not allocated from heap, but from stack, and this is why the limit is so low."
Two caveats about that source, for fairness: the primary subject of that issue
is a different bug (double quotes being lost when noglob is enabled), and the
passage above is context within it. Note also that the trigger set includes both
quote characters, which is why ordinary shell one-liners take this path at all.
Symptom 2 is measured, but its cause is my inference. I could not find any
source tying the backslash collapse to this routine; neither reference below
mentions backslash handling. My hypothesis is that the same argv rebuild
applies Windows escaping rules, where a backslash preceding a quote is an
escape. Treat that as unconfirmed. The measurement itself is reproducible (see
the repro section) and does not depend on the hypothesis being right.
This is not a bug in bash (which is OS-agnostic), nor in Windows (which delivers
32,767 fine), nor strictly in Claude Code. It is the MSYS2 glue between them.
But Claude Code is what makes it reachable, by transporting arbitrary
user-sized payloads through argv.
References:
- https://github.com/msys2/msys2-runtime/issues/178 (the stack-buffer mechanism)
- https://github.com/zetaloop/msys2-argv-fix (independent description, plus a
LD_PRELOAD workaround that rebuilds argv from GetCommandLineW)
- https://github.com/openai/codex/issues/15003 (same class of bug in another
tool: a large payload transported via argv on Windows, with the same
WinError 206. To be precise, the failure mode reported there is a complete
failure before execution, not a partial cut)
Why the effective budget is well under 8,203
The command is not passed through as written. It is wrapped roughly like this:
bash -c "source <shell snapshot> && export TEMP=... && shopt -u extglob
&& { unalias ...; } && eval '<USER COMMAND>' && pwd -P >| <cwd file>"
Two consequences:
- The wrapper consumes roughly 1 KB before the user command starts.
- Because of
eval '...', every single quote in the user command must be
escaped, and the escape used turns 1 character into 5 ('"'"').
Prose with apostrophes (won't, $(date '+%F')) burns the budget much faster
than its apparent length.
That is why failures in practice start at ~8,000 bytes of user command rather
than at 8,203.
Suggested fix
Pass the command on bash's stdin instead of as the -c argument. In my tests
this removes both symptoms completely: byte-identical output up to 259 KB, with
backslashes preserved. Equivalent alternative: write the command to a temp file
and run bash <file>.
Either way the payload stops traversing argv, which is the only thing the
MSYS2 bug touches.
What does not work
MSYS=noglobdoes raise the-climit from 8,203 to 32,731, because it
disables the globbing path that owns the stack buffer. But it breaks argv
reconstruction whenever quotes are present, and the command dies with
e: eval: line 1: unexpected EOF while looking for matching. Given the
eval '...' wrapper is always present, this trades a size limit for guaranteed
corruption. Tested and rejected.
- Chunking the payload fixes the truncation but does nothing for the
backslash collapse, which happens at 295 bytes.
- Base64-encoding the payload makes it worse: it is still an
argvpayload,
now ~33% larger.
Reproduction
Self-contained, no Claude Code needed. chr(92) is used deliberately so the
repro script itself cannot be corrupted by the bug it tests.
import subprocess, os
BASH = r"C:\Program Files\Git\bin\bash.exe"
B = chr(92)
def run(mode, payload):
cmd = "cat > out.txt <<'EOF'\n%s\nEOF\n" % payload
if mode == "-c":
esc = cmd.replace("'", "'" + '"' + "'" + '"' + "'") # the eval wrapper
return subprocess.run([BASH, "-c", "eval '%s'" % esc])
return subprocess.run([BASH, "-s"], input=cmd, text=True)
# 1) backslash collapse, tiny payload
body = "regex: r'" + B + B + "d+'"
for mode in ("-c", "stdin"):
if os.path.exists("out.txt"): os.remove("out.txt")
run(mode, body)
got = open("out.txt").read().strip()
print(mode, "->", got, "| OK" if got == body else "| CORRUPTED")
# 2) truncation threshold
line = "- note: the disk won't mount, and the driver's log is unhelpful"
for n in (7000, 9000):
if os.path.exists("out.txt"): os.remove("out.txt")
big = (line + "\n") * (n // len(line))
r = run("-c", big)
print("-c with", len(big), "bytes -> exit", r.returncode)
Expected on Windows with Git Bash: case 1 prints CORRUPTED for -c and OK
for stdin; case 2 exits 0 at 7,000 bytes and exits 2 withunexpected EOF while looking for matching at 9,000 bytes.
Related issues
I opened each of these before filing. None reaches the argv/size mechanism, which
is why I am filing separately instead of commenting there.
- #26742 (closed, not planned) and its duplicates #26946 and #27163:
same visible error message, but a different and real cause, an apostrophe in the
Windows username (C:\Users\SeanO'Loughlin). Worth noting that the reporter of
#27163 explicitly rules out the path theory in the body, having reproduced the
error on macOS. None of the three mentions payload size, argv or 8192.
- #48317 and #29619: both show
unexpected EOFwhen writing files or when
passing markdown to gh. Both were closed automatically for inactivity, and the
discussion in them points at heredoc quoting of markdown content, not at size.
So this error message already has at least one confirmed unrelated cause. That is
worth keeping in mind when triaging: what distinguishes the mechanism reported
here is that the payload passes bash -n intact, and that the reported line
number stays constant across payload sizes.
Happy to run further measurements on this machine if useful.