[BUG] Windows/Git-Bash: valid shell snapshots fail in host bash -c wrapper (recurrence of #68337)
Windows/Git-Bash: clean shell snapshots still fail in the host bash -c wrapper
Summary
An active Claude Code-backed session on Windows/Git Bash fails before the requested command runs:
/usr/bin/bash: -c: line 87: unexpected EOF while looking for matching quote
This is a fresh recurrence/follow-up to #68337, which was auto-closed as stale on 2026-07-26 and asks affected users to open a new issue.
Environment and scope
- Windows 11, Git for Windows / Git Bash.
- Installed Claude Code CLI:
2.1.211(claude --version, 2026-07-27). - The failure was observed through Nimbalyst's Claude Code provider integration. I have not spent provider capacity to reproduce it in a bare interactive
claudesession, so this report is evidence for triage rather than a claim that Nimbalyst is definitively excluded.
Evidence that narrows the fault
The generated snapshot files were audited locally, without altering them:
- 322 files under
%USERPROFILE%\\.claude\\shell-snapshotspassedbash -n. - The same 322 files passed clean non-interactive sourcing with
bash --noprofile --norc. - The latest snapshot also sourced with
set -Henabled. Its Windows path content is inside a balanced single-quoted export.
So the snapshot data itself neither has a syntax error nor fails when sourced. The error instead arises from the unseen command string assembled around snapshot sourcing: the tool reports bash -c line 87 even though the requested operation is short. This matches the failure family in #68337 (then line 111), whose direct-bash vs host-wrapper contrast reached the same conclusion.
Expected behavior
Snapshot replay and command plumbing must not turn valid shell state plus a valid requested command into an unterminated quote. A parse failure in host-added preamble must be clearly surfaced and must not poison subsequent commands.
Requested investigation / repair
- Make snapshot sourcing argument-safe: invoke the snapshot by positional argument (for example, a fixed
bash -c 'source "$1"' ...program) rather than interpolating a snapshot path or content into an assembled shell command. - Isolate/reset the persistent shell after a host-wrapper parse error, and retry only in a fresh process where safe.
- Surface the assembled wrapper, at least behind a debug flag, so a malformed host-added line can be diagnosed.
- Add Windows/Git-Bash coverage for snapshot paths/content containing Windows-style paths and quotation-sensitive values.
Relation to #68337
This is not evidence that the prior root-cause hypothesis is complete, but it is strong recurrence evidence: the same wrapper-only unexpected EOF signature persists in current CLI 2.1.211 while on-disk snapshots are demonstrably valid. #68337 was closed only for inactivity, not resolved with a verified fix.
5 Comments
Session: 67bc28dd-3b42-495e-869f-018207237656 (ChiChi custody, 2026-07-29)
Claude Code: 2.1.211
Host: Windows 10, Nimbalyst editor
This session independently reproduced the fault via trivial command (
echo "Test 1: trivial")while running INSIDE a live Nimbalyst-hosted Claude Code session (not a bare CLI invocation).
Key corroboration:
/usr/bin/bash: -c: line 87: unexpected EOF while looking for matching ''Evidence gap addressed:
Original filing flagged lack of bare-CLI reproduction. This session provides
corroborating evidence from within Nimbalyst's hosted Claude Code provider, confirming
the fault is reproducible in the intended execution context even if bare-CLI reproduction
remains elusive.
Same failure family reproduced on Windows 11, Git Bash, CLI 2.1.220 (latest as of 2026-07-31). Our case shows BOTH defects at once — the truncated-snapshot write from #68066 (closed as stale) and the wrapper-side failure this issue describes:
~/.claude/shell-snapshots/is truncated at exactly 7194 bytes, mid-line inside the finalexport PATH='...— an unterminated single quote. Restarting the CLI regenerates a new snapshot with the same 7194-byte cut. This is the #68066 mechanism (partial snapshot kept and used), except the cut lands inside the PATH export itself, so every Bash call fails to parse.bin/directories Claude Code itself appends. That is what pushes the PATH line past the cut point.unexpected EOF while looking for matching quote(line number pointing into the snapshot's PATH line). Later the tool degrades further: it executes nothing at all while reporting success — no output, no side effects (a probe file-create produces no file), yet exit codes look clean. This silent-fabrication stage is the dangerous one, since scripts appear to have run.bash -nexit 0, file confirmed untouched afterwards) does not revive the tool mid-session — consistent with this issue's conclusion that the fault also lives in the assembled hostbash -cwrapper, not only in the snapshot bytes.Suggested fixes from #68066 still apply (atomic temp-file + rename, validate before use, deliver plugin bin paths via spawn env), plus this issue's request to make snapshot sourcing argument-safe.
Update (same day): after slimming plugins the snapshot is now complete and valid (sources clean in Git Bash, ash -n exit 0), CLAUDE_CODE_GIT_BASH_PATH is set explicitly to C:\Program Files\Git\bin\bash.exe, CLI restarted on 2.1.220 - and the Bash tool still executes nothing while reporting success. With the snapshot ruled out entirely, the remaining fault is the assembled host wrapper, confirming this issue's hypothesis.
Follow-up with new evidence, reproducing this independently via a Nimbalyst-hosted Claude Code session (Windows, git-bash backend, CLI 2.1.222).
Falsifies the shell-snapshot-truncation mechanism for this exact
-c: line Nsignature. Built a deliberately truncated file (export PATH='/a/b/cwith no closing quote) and sourced it inside abash -c "source file.sh"wrapper. The resulting error is attributed to the sourced file's own name (file.sh: line 3: unexpected EOF...), never to-c. Every real incident I've seen (this session and two prior internal investigation rounds) is consistently attributed to-c: line N, not to a snapshot filename. A genuinely truncated/corrupted on-disk snapshot being sourced does not produce this exact signature — so for the reports that say-c: line N(as opposed to<snapshot-file>: line N), the truncated-snapshot theory doesn't fit, whatever else may be going on with snapshot files. My own snapshot files are also confirmed healthy at rest (bash -nclean, 4.5-4.6KB, nowhere near a 7194-byte truncation point).Live-captured the actual host-assembled wrapper (via a Win32 process-table poll racing a live Bash tool call) for what I believe is the first time in this thread. Single logical line, structure:
The user/assistant's command text is embedded verbatim inside a single-quoted
eval '...'.Found a 100%-reliable, on-demand reproduction that follows directly from that wrapper shape: a command containing a double-quoted Windows path ending in an odd number of backslashes immediately before the closing
", e.g.:Through the real Bash tool:
/usr/bin/bash: eval: line 4: unexpected EOF while looking for matching"'(exit 2). And critically — this reproduces **byte-for-byte identically with a bare, completely unwrappedbash -c '<command>'`**, zero harness involvement:This is standard bash double-quote semantics (inside
"...", a backslash immediately before"escapes it, so the quote never closes) — not a harness escaping bug. Since the command is embedded verbatim ineval '<cmd>', the reported line number is simply wherever in the (often long, multi-line) command text this pattern happens to land — which is consistent with different reports citing different line numbers (87, 88, ...) for what's structurally the same bug, and explains why short/trivial reproduction attempts kept missing it (none happened to end a double-quoted path in a lone trailing backslash).I did not find a reproduction for the separate "silent success, executes nothing" variant reported upthread, and my (limited) testing today weakly argues against session-wide poisoning for this specific trigger class — after tripping the EOF error live through the real Bash tool, my very next Bash tool call in the same session succeeded normally, and a Win32 process-table capture during live calls showed fresh
bash.exeprocesses spawned per call rather than one persistent shell accumulating state. I'm not claiming that generalizes to every reported case, just flagging it as a data point.Workspace-side mitigation (not a fix to the harness itself, which is closed-source): a pre-flight hook that runs
bash -non the exact command text before it reaches the real Bash tool, denying with a concrete fix hint if bash's own parser would reject it. Verified live against the reproduction above.Happy to share the full write-up (elimination ledger + wrapper capture details + fix) if useful.
Follow-up with further evidence and a likely (but not fully confirmed) explanation for most of the
-c: line Nreports in this thread, plus a concrete ask.Likely primary driver: unbounded env-file growth in a Claude Code plugin, not a bash quoting bug
Traced a large share of these incidents to a specific plugin defect, already filed and being actively fixed upstream: openai/codex-plugin-cc#528. Summary: a Codex-Companion-family plugin's
SessionStarthook appends 3 env-export lines to a shared env file on everySessionStartfiring — including resume and compaction re-fires, not just a genuine fresh launch — with no dedup (fs.appendFileSync, unconditional). That file's content gets embedded literally into every subsequent Bash-tool-c "..."wrapper. Over a long session lineage this grows without bound: I independently observed 30x repetition (an 88-line composed wrapper) in one session and 74x repetition (223 lines) in a second, longer-running session on the same machine, confirmed by direct source read of the plugin'sappendEnvVar/handleSessionStartfunctions (byte-identical in the stock package and a locally-modified fork, so not fork-specific).That issue's own author independently bisected the trigger to the Windows 8191-character command-line limit on their system. I want to be precise about what I could and couldn't confirm on mine: I captured several of my own composed
-cwrapper strings live (Win32_Process poll during real failures) and replayed the isolated text directly throughbash.exe, bypassing the Bash tool entirely. This did not reproduce the exact EOF signature in any of my samples, regardless of whether the captured text was above or below 8191 characters — some over-8191 captures ran to completion (with an unrelated quoting defect, below); one under-8191 capture failed a different way (cascading command errors, not an EOF). So while the general shape (longer accumulated prefix -> more failures) clearly holds — a session with 74x repetition and a ~20,700-character wrapper failed on effectively every Bash-tool call regardless of command content, including commands with zero quote characters, which a pure length effect explains cleanly — I can't personally confirm the exact 8191-character mechanism reproduces via offline replay. That's consistent with, not a resolution of, the gap below.Standing gap: captured wrapper text isn't proven identical to what bash actually parses
Every wrapper string I've captured via Win32_Process during a live failure — long or short, over or under any length boundary I tried — passes
bash -ncleanly, and replaying it directly throughbash.exenever reproduces the live "unexpected EOF"/"unexpected end of file" signature; it either completes (see the quoting defect below) or fails a different, unrelated way. That gap — clean/replayable in isolation, broken live — means something between however Claude Code composes the wrapper and bash.exe's realargv[2]isn't visible to any capture method I have available. I don't have a confirmed mechanism for that gap; flagging it precisely rather than guessing further.A second, separate, directly-demonstrated defect
Independent of the above: when a Bash-tool command contains a literal
"character, I captured a case where it got embedded into the wrapper's single-quotedeval '...'with the"backslash-escaped (\"). Backslash-escaping a double quote has no delimiting effect inside single quotes — it's just a literal\followed by a literal"— so this doesn't protect the embedded command's own quoting, it corrupts it. Replaying the isolated captured text showed this directly: on one command it ran to completion with the stray quote characters preserved literally in the output instead of stripped; on a more complex (multi-line,"-and-'-mixed) command it cascaded into unrelated downstream parse errors instead. Neither replay reproduced the EOF signature either — this defect is real and worth fixing, but on its own it doesn't explain the exact failure mode reported in this thread. I checked the Codex-Companion plugin's own editable source for this transform and didn't find it there, so I believe it lives in Claude Code's own wrapper-composition code.What would actually close this: one instrumentation ask
Both findings above come from indirect measurement — process-table capture, character counts, offline replay — not a direct observation of the bytes bash.exe receives at the moment of a live failure. Is there an existing debug/trace flag in Claude Code that dumps the exact final command-line string at the point it's handed to the child-process spawn call, downstream of any Node-side string composition? That single instrumentation point is the one thing that would move this from "well-supported inference with one confirmed contributing defect" to "directly observed." Also worth asking on the Codex-Companion plugin side, since it composes part of the environment that ends up in that string, though the escaping defect above looks like core Claude Code behavior from what I could find in the plugin's source.
Hypotheses checked and ruled out along the way
For anyone else debugging a similar-looking report — these looked plausible early on and were directly falsified, not just deprioritized:
-c. Every real incident I've seen is-c-attributed. Snapshot files at rest were also confirmed healthy (bash -nclean).TEMP/TMPenvironment values containing a stray quote character: checked directly (value, regex match, UTF-8 byte dump) on an affected session — clean stock values, nothing stray.Where this leaves my first comment (H1, the double-quote/trailing-backslash bug)
Still real, still fixed workspace-side with a pre-flight
bash -ngate — but it's now clear it wasn't the main driver of the incidents that originally motivated this thread. It fires on a narrower, specific trigger (a double-quoted Windows path ending in an odd number of backslashes) than the accumulation-driven failures above, and produces a distinguishably different bash error format when it fires through the real harness (eval: line N, not-c: line N).Happy to share raw captures/measurements if useful — will update here once the instrumentation question above is answered one way or the other.
Update on the second defect from my last comment (the
"->\"escaping mismatch): a fresh, never-bloated session ran the exact double-quoted-Windows-path shape and the mixed-quotepython -ctrigger from that comment, plus this thread's original command shapes -- 4/4 ran clean, no failure at all. Not what I expected.The escaping transform itself is still an observed fact, not in question -- I captured it directly in a live wrapper and replayed the isolated text to show the mechanism concretely. What's now genuinely uncertain is whether it's independently sufficient to cause a failure on its own. Every prior demonstration of it (including the one in my last comment) ran on a session that was also accumulating the unbounded env-block growth I described -- so what looked like "this transform causes an EOF" may actually have been that accumulation doing the real work, with the escaping mismatch riding along as a real but non-fatal quirk. Or it's real and conditional on something 4 test samples didn't hit -- length, nesting depth, timing. I can't distinguish those two with what I have.
Flagging this so it doesn't get treated as more settled than it is: a length/repetition-driven fix (removing the accumulation) might turn out to remove this failure mode too, without the escaping mismatch itself ever being touched.