[Bug] Bash tool silently strips backslashes on Windows/Git Bash

Status Open
Maintainer reply None cached
Activity 9 comments · opened Aug 25, 2026

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

[]

View original on GitHub ↗

3 Comments

Avick3110 · 1 month ago

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 with defaultMode: "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.

clarejor · 1 month ago

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 PreToolUse hook on Bash that just logs tool_input.command, then running:

printf '[%s]' '\' '\\' '\\\\' '\\\\\\\\' '\n' '\\n' 'C:\Users\me'

The hook logs that command with every backslash intact. What bash actually prints is:

[\][\][\\][\\\\][\n][\n][C:\Users\me]

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:

printf 'RESULT=[%s]' '\\'; tr '\0' '\n' < /proc/$$/cmdline | tail -1

prints RESULT=[\], and the wrapper argv comes back as:

... && eval 'printf '"'"'RESULT=[%s]'"'"' '"'"'\'"'"'; ...' && pwd -P >| ...

Two things are true of that single string at the same time:

  • every ' in my command was escaped correctly, to '"'"'
  • my \\ 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 PreToolUse
hook can pre-double the backslashes so the two cancel:

# PreToolUse, matcher: Bash
payload = json.load(sys.stdin)
cmd = payload.get("tool_input", {}).get("command", "")
if payload.get("tool_name") == "Bash" and chr(92) in cmd:
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "updatedInput": {"command": cmd.replace(chr(92), chr(92) * 2)},
    }}))

With that in place, heredocs, sed -i 's/x/A\\nB/', awk '/\\/' and Windows paths all
behave 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 and
re-encoded. Reproduce with printf '%s' 'X' | od -c substituting any non-ASCII character
for X. Happy to open that as its own issue if it isn't already tracked.

AroraZbar · 1 month ago

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's main() 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=noglob in the environment the halving stops, and the \" handling stops with it, so any command containing a double quote dies before it runs:

printf '%s\n' 'M\\N' | od -c     # MSYS=noglob: M \ \ N   (halving gone)
echo "x"                          # MSYS=noglob: unexpected EOF while looking for matching `"'

A receiver-side knob would not move a defect that lived in claude.exe's own quoting routine. So MSYS=noglob is 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 with bash -s. Either sidesteps the MSYS2 argv step entirely, and also removes the 8,191-character command-line cap that the eval '...' 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.

Showing cached comments. Read the full discussion on GitHub ↗