[Bug] Bash tool silently strips backslashes on Windows/Git Bash
Bug Description
Bash tool silently collapses \\ to \ in commands on Windows/Git Bash
## Summary
On Windows, the Bash tool destroys one backslash from every \\ pair in the
command string, somewhere between the stored tool input and the bash argv.
Bash never receives the second backslash, so no amount of correct quoting in
the command can protect the content — including a quoted heredoc (<<'EOF'),
which by POSIX must do zero processing.
This is silent. Writing C# via a heredoc turns "tools\\bench.cmd" into
"tools\bench.cmd". \b is a valid C# escape (backspace), so it compiles
clean and tests pass; the defect surfaces later at runtime. JSON would fail
loudly, C#/regex will not.
## Environment
- Claude Code 2.1.243 (latest at time of report)
- Windows 11 Pro 10.0.26200
- GNU bash 5.3.15(1) (x86_64-pc-cygwin), MINGW64_NT-10.0-26200
- git 2.55.0.windows.3, shell /bin/bash.exe
- permissions.defaultMode: "auto"
## Reproduction
Single Bash tool call:
printf '%s' 'M\\N' > /tmp/bs.txt; od -c /tmp/bs.txt
Expected: M \ \ N
Actual: M \ N
Same result with a quoted heredoc, which should be fully literal:
cat > /tmp/hd.txt <<'EOF'
var p = "tools\\bench.cmd";
EOF
od -c /tmp/hd.txt # -> tools\bench.cmd
## Evidence: chain of custody for one backslash
Three measurement points on the same printf '%s' 'M\\N' call:
| stage | backslashes | measured by |
|----------------------------------------|-------------|------------------------|
| model -> harness, Bash tool input | 2 M\\N | session JSONL via jq |
| model -> harness, Write tool (control) | 2 | session JSONL via jq |
| harness -> bash argv | 1 M\N | /proc/$$/cmdline |
| resulting file | 1 M\N | od -c |
The session transcript holds two backslashes, so the model emitted them and
the harness received them. /proc/$$/cmdline — the literal argv bash was
launched with — holds one. Bash cannot have eaten a byte it never got.
To reproduce the argv half, add to the same call:
cat /proc/$$/cmdline | tr '\0' '\n'
and to reproduce the transcript half:
jq -r '.message.content[]? | select(.type=="tool_use")
| select(.input.command? // "" | contains("bs.txt")) | .input.command' \
~/.claude/projects/<project>/<session-id>.jsonl | od -c
## Signature
- \\ -> \ (collapsed)
- \", \t, \d, \s, lone \ -> unchanged
- \\\\ -> \\ (so exactly one level is stripped)
Independent of quoting: reproduces inside single quotes, inside <<'EOF',
and with no quotes at all.
That pattern looks like un-doubling of escaped Windows path separators being
applied to the entire command string rather than only to paths. The bash
invocation on Windows is wrapped in a Windows-specific preamble
(export TEMP='C:\Users\...' ... && eval '<command>') that handles paths with
single backslashes, which seems the likely home for it — though I've localized
this by measurement, not by reading the code.
## Ruled out
- Bash quoting: reproduces where POSIX guarantees literal handling.
- The eval wrapper: the doubled backslash is already gone in argv, before
eval runs.
- Model-side escaping: the Write tool control in the same session carried
\\ through intact, and the transcript shows the Bash input arrived with
both backslashes.
## Impact
Wider than file writing — any Bash command carrying \\ is affected,
including sed scripts and grep patterns. While investigating, my own
diagnostic grep -oE 'M\\+N' arrived as M\+N and silently matched nothing.
Severity is amplified by defaultMode: "auto", whose standing instruction is
to "make file changes with sed, heredocs, or short scripts, rather than using
the dedicated Read, Edit, or Write tools." That routes file authoring through
the defective path by default. On a C#/JSON codebase this corrupts source
silently and at scale.
Agents cannot work around it: their quoting is already correct, and the bytes
are destroyed after the model is done. The only mitigation is avoiding Bash
for backslash-bearing content, which is in direct tension with auto mode.
Environment Info
- Platform: win32
- Terminal: windows-terminal
- Version: 2.1.243
- Feedback ID: 3eaf988c-6f7d-401b-b35c-32351f301e03
Errors
[]Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Still reproduces on 2.1.245, which postdates both this report (2.1.243) and #88561 (2.1.224). Independently measured today on Windows 11 26200 / Git Bash 5.3.15 (MINGW64),
defaultMode: "auto".Same signature, confirmed by byte count rather than by eye:
| typed | arrives at bash |
| --- | --- |
|
a\b|a\b||
a\t|a\t(not a tab — so not a JSON decode) ||
a\\b|a\b||
a\\\\b|a\\b|| 8 backslashes | 4 |
Tool-by-tool fidelity, same machine, same session:
| tool | faithful? |
| --- | --- |
| Bash | no — halves every
\\pair || PowerShell | yes (1→1, 2→2, 4→4) |
| Write | yes |
One thing worth adding for anyone triaging by severity: the auto-mode interaction in this report's Impact section is not a corner case, it is the main way people meet this bug. Auto mode's standing instruction is to prefer
sed/heredocs/short scripts over Read/Edit/Write, so on Windows every routine file write is routed through the one transport that corrupts backslashes. A user withdefaultMode: "auto"hits this several times a session and reads it as "heredocs are broken"; a user without it may never hit it at all. That difference in exposure is probably why a Windows-wide transport bug has taken this long to get two reports.The practical consequence is that the documented workaround — double every backslash — does not survive contact with a long session. It asks the model to keep an escaping rule in its head across every subsequent command, while the surrounding system prompt is actively steering it toward the defective tool. Mitigation that holds is structural: use Write for backslash-bearing content, or forward slashes, or a script file. Counting is not a fix, it is a rule waiting to be forgotten.
Confirming this on a different Windows setup, with two measurements I haven't seen in this
thread or the related ones.
Environment: claude.exe 2.1.247, entrypoint
claude-desktop, Git Bash 5.2.37(1)(MINGW64), Windows 11. Note that 5.2.37 is older than the 5.3.x reported here and in
#88561, and the behaviour is identical, so the Git Bash version looks irrelevant.
1. A PreToolUse hook receives the command verbatim
Registering a
PreToolUsehook onBashthat just logstool_input.command, then running:The hook logs that command with every backslash intact. What bash actually prints is:
So the loss happens strictly after hook dispatch and before the bash process starts. That
rules out the model output, the transcript, and the hook layer itself. This corroborates
@emel-purely's measurement in #85856 from an independent environment.
2. In the same wrapper string, quote-escaping is correct while backslash-escaping is not
Reading the real argv from inside the command:
prints
RESULT=[\], and the wrapper argv comes back as:Two things are true of that single string at the same time:
'in my command was escaped correctly, to'"'"'\\had already been reduced to\I think this is difficult to reconcile with the MSVCRT-vs-MSYS2 receiver theory in #85856.
A decoding mismatch on the receiving side would not be selective about which characters it
mangles, and in particular would not leave the single-quote escaping intact while dropping
backslashes. It looks more like a quoting routine inside claude.exe that handles quote
characters but not backslashes, running upstream of process spawn.
Workaround, for anyone blocked
The transform is exact and invertible: even-length backslash runs are halved, a lone
backslash survives. Since the command is still intact at the hook boundary, a
PreToolUsehook can pre-double the backslashes so the two cancel:
With that in place, heredocs,
sed -i 's/x/A\\nB/',awk '/\\/'and Windows paths allbehave as written. It is a workaround rather than a fix, and it has to be removed once this
is corrected upstream, or it will double backslashes that are no longer being halved.
Separate but adjacent defect
The same command-building path also corrupts non-ASCII. A single em dash (UTF-8
E2 80 94)arrives at bash as
C3 A2 E2 82 AC E2 80 9D, i.e. the UTF-8 bytes decoded as cp1252 andre-encoded. Reproduce with
printf '%s' 'X' | od -csubstituting any non-ASCII characterfor X. Happy to open that as its own issue if it isn't already tracked.
Same defect on Claude Code 2.1.233, Windows 11 Pro 26200, Git Bash (MSYS2 runtime),
defaultMode: "auto". Two measurements that I think reconcile the argv findings above with the MSYS2 theory in #85856, plus a scale figure.1. An MSYS2 runtime knob changes the outcome, so the halving happens in the MSYS2 command-line-to-argv step. The harness launches
bash.exe -c "<preamble> && eval '<command>'": the whole script is ONE double-quoted element of the Windows command line. The MSYS2 (Cygwin) runtime rebuilds argv from that command line before bash'smain()runs. Measured here, inside a double-quoted segment that step rewrites\\to\and\"to"and leaves every other backslash alone, which is exactly the signature in this thread (\t,\d, lone\untouched). It is selective by design, and that is why the'"'"'quote-escaping survives while\\does not: two different layers do those two things.Receipt: with
MSYS=noglobin the environment the halving stops, and the\"handling stops with it, so any command containing a double quote dies before it runs:A receiver-side knob would not move a defect that lived in claude.exe's own quoting routine. So
MSYS=noglobis not a workaround, but it does locate the bug.2. Scale. Transcript telemetry across two Windows machines, 7 days to 2026-09-09: 433 Bash tool calls contained
\\, and every one of them reached bash with a backslash removed. Typical silent outcomes: Python source written through a heredoc landed with a real tab where"\\t"was intended, and Windows paths lost half their separators.Fix suggestion. Keep the script out of the Windows command line: write it to a temp file and run
bash <file>, or feed it on stdin withbash -s. Either sidesteps the MSYS2 argv step entirely, and also removes the 8,191-character command-line cap that theeval '...'wrapper hits today. The pre-doubling hook above works, but it has to track the launch shape and it counts double against that cap.Our own mitigation until then is a PreToolUse hook that rejects any Windows Bash call containing
\\and points the model at Write/Edit or the PowerShell tool, both of which pass backslashes intact.