[BUG] Plugin `bin/` entries are added to PATH with no existence check — 74% of PATH becomes dead dirs, pushing the inlined shell snapshot past the Windows 8191-char limit

Status Fixed / completed
Reported on v2.1.278
Maintainer reply None cached
Activity 2 comments · opened Sep 20, 2026 · closed Sep 25, 2026

Summary

Two defects compound into "every Bash call in a session fails with unexpected EOF while looking for matching \'\`":

  1. **dVn() adds <plugin.path>/bin to the Bash tool's PATH for every enabled non-builtin plugin, with no existence check.** Most plugins ship no bin/ directory at all, so the PATH fills with dead entries.
  2. The snapshot is inlined into a single bash -c "<string>" command line, which on Windows is subject to the ~8191-char cmd.exe limit. Excess PATH length silently truncates it mid-export PATH='...', leaving an unterminated quote.

This report adds source-level root cause + exact measurements that I could not find in the existing reports (#31799, #38951, #68066, #81356, #88311, #90421).

Environment

  • Claude Code 2.1.278 (VS Code extension anthropic.claude-code-2.1.278-win32-x64); npm package 2.1.274; npm latest is also 2.1.278
  • Windows 11 Home China 10.0.26200
  • Shell: Git Bash (Git for Windows / MSYS2)
  • 26 enabled plugins (mix of claude-plugins-official marketplace and a local marketplace)
  • cmd.exe-launched Claude processes are also present (via a bridge tool that spawns cmd /c claude --output-format stream-json ...)

Defect 1 — no existence check in the plugin bin PATH builder

De-minified from the 2.1.274/2.1.278 native binary:

function dVn(e){
  let {enabled:n} = await Ni(e);
  return n
    .filter(r => !r.isBuiltin && r.path)          // every enabled, non-builtin plugin
    .map(r => Hc(r.path, "bin"))                  // -> <plugin.path>/bin  — no fs.existsSync
    .filter(r => {
      if (HE !== "\\" && /[:"'$`\\\n\r]/.test(r))
        return t(`Dropping plugin bin path with shell metacharacters: ${r}`), !1;
      return !0
    })
}

Notes:

  • There is no existsSync/statSync anywhere in this path — the directory does not have to exist.
  • HE is the path separator, so on Windows (HE === "\\") even the shell-metacharacter filter is skipped, because Windows paths legitimately contain :.
  • Related strings in the same binary: Shell snapshot: leaving plugin bin/ dirs off PATH: (only reached when this computation throws), plugin bin/ PATH entries changed, plugin bin/ PATH entries unchanged.
  • Still present in the latest release. Comparing the 2.1.274 and 2.1.278 native binaries, the function is logically identical — only the minified identifiers changed (Ni→Fi, Hc→ka, HE→Sh):

``js
function _Xn(e){let{enabled:n}=await Fi(e);return n.filter((r)=>!r.isBuiltin&&r.path)
.map((r)=>ka(r.path,"bin"))
.filter((r)=>{if(Sh!=="\\"&&/[:"'$
\\\n\r]/.test(r)) … })}
```

So upgrading to 2.1.278 does not change this behaviour. The changelog for 2.1.275–2.1.278 contains no entry touching plugin bin/ PATH entries or snapshot truncation.

Measured effect

26 enabled plugins → 26 PATH entries, and all 26 directories do not exist (os.path.isdir on each; also confirmed by listing the plugin roots — e.g. plugins/skill-creator/ contains only LICENSE, README.md, skills/; the manifests declare no bin field at all).

PATH total            3212 chars
plugin bin entries    26 entries = 2386 chars = 74% of PATH
of which exist        0
average per plugin    ~93 chars

Across the whole plugin tree there are only 2 bin/ directories, and neither is a plugin-root bin (data-agent-kit-starter-pack/0.6.1/mcp/bin, data/dash0-.../bin).

Defect 2 — PATH length is on the critical path to an 8191-char truncation

Measured on this machine, snapshot_file_bytes = PATH_chars + 1636 (constant verified across two snapshots):

| snapshot | PATH chars | segments | file bytes | notes |
|---|---|---|---|---|
| normal (6 samples) | 3028 | 51 | 4679 | healthy |
| normal (16:55 today) | 3028 | 51 | 4679 | healthy |
| doubled (17:18 today) | 5519 | 84 | 7170 | 24 bytes from the cliff |

Truncation occurs at ~7194 bytes of snapshot; 7194 + wrapper ≈ 8191 = the Windows cmd.exe command-line limit (this matches the arithmetic in #90421).

The doubling factor

The plugin bin block can appear twice. The ordering in the doubled snapshot proves where the first copy comes from:

normal (51 seg):  .......................pp PPPPPPPPPPPPPPPPPPPPPPPPPP
                                        ^^  perl dirs (added by /etc/profile at shell start)
                                           ^ plugin block — appended by Claude Code, LAST

doubled (84 seg): .............................. PPPP...PPPP pp PPPP...PPPP
                                               ^plugin block  ^perl  ^plugin block

The first plugin block sits before the /etc/profile-added perl dirs, so it can only have been inherited from the parent process environment — i.e. a Claude Code session (or a tool bridging one) being launched from a shell that already had Claude Code's plugin block in PATH. Claude Code then appends its own block again.

Live reproduction (this machine, 2026-09-20 17:18):

claude.exe     17:18:04   ← produced the 7170 B / 84-segment snapshot at 17:18:16
 └ cmd.exe     17:18:04   cmd /c claude --output-format stream-json ...
   └ cc-connect.exe  17:09:30   (a bridge tool: Claude Code <-> messaging platforms)
     └ node.exe  17:09:29
       └ sh.exe  17:09:29   Git Bash

Sessions launched by Code.exe on the same machine produced clean 4679 B snapshots; the only cmd.exe-parented one doubled. This matches the repro sketch in #88311 ("launch VS Code from an already-extended shell").

Why the user-visible failure is "long commands first, then everything"

Every enabled plugin costs ~93 chars of a fixed budget (~5558 chars of PATH once the 1636-byte snapshot overhead and the ~1000-byte wrapper are subtracted). The budget is:

8191  cmd.exe limit
−1636 snapshot non-PATH overhead (header + rg/pkill shadow functions)
−~1000 harness prologue/epilogue
≈5558  PATH budget
−826  system PATH (fixed)
=4732 for plugins ≈ 50 plugins at ~93 chars each — if PATH is not doubled
                            ≈ 25 plugins — if it is doubled

With 26 plugins, the doubled case lands 24 bytes from truncation, so one more plugin (or one more duplicated system dir) tips it over.

Repro

  1. Windows 11 + Git Bash, enable ~25 plugins that ship no bin/ directory (most do not).
  2. echo $PATH | tr ':' '\n' | grep -c plugins → one dead entry per enabled plugin.
  3. Launch a Claude Code session from a shell that already has those entries in PATH (e.g. a bridge/automation tool started from inside a Claude Code session).
  4. ls -t ~/.claude/shell-snapshots/*.sh | head -1 → the new snapshot is ~7170 B with a doubled plugin block.
  5. Any Bash call then fails with unexpected EOF while looking for matching \'\`` once PATH grows slightly more.

Suggested fixes

  1. Gate the plugin bin PATH entry on existence: fs.existsSync(Hc(r.path, "bin")) before adding it. This alone removes 74% of PATH on this machine and costs nothing.
  2. Do not skip the metacharacter filter on Windows based only on the separator — the current HE !== "\\" guard disables it for every Windows path.
  3. Deliver plugin bin paths via the spawn environment instead of the inlined snapshot (also proposed in #68066), so PATH growth cannot push the composed bash -c string over the command-line limit.
  4. Validate the composed snapshot/script before use, or write it atomically, so a truncated/partial snapshot surfaces as an error instead of as unexpected EOF in every subsequent call.
  5. Consider de-duplicating the plugin block when the parent environment already contributed it.

Prior reports (same family, none fixed)

| Issue | Date | State |
|---|---|---|
| #31799 — literal $PATH in snapshot | 2026-03-07 | closed stale |
| #38951 — literal $PATH (duplicate) | 2026-03-25 | closed stale |
| #68066 — truncated snapshot drops plugin bin PATH entries | 2026-06-12 | closed stale, locked 2026-09-19 |
| #81356 — snapshot PATH export contains only the plugin bin dir | 2026-07-26 | closed stale |
| #88311 — inlined snapshot + wrapper exceeds command-line length | 2026-08-20 | open, 0 comments |
| #90421 — snapshot silently truncated at ~7.2 KB on Windows | 2026-08-28 | open, 0 comments |

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗