anthropics / anthropics/claude-code
[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
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 147k
- Forks
- 24k
- PR merge metrics
- PR metrics pending
Description
Summary
Two defects compound into "every Bash call in a session fails with unexpected EOF while looking for matching \'`":
dVn()adds<plugin.path>/binto the Bash tool's PATH for every enabled non-builtin plugin, with no existence check. Most plugins ship nobin/directory at all, so the PATH fills with dead entries.- The snapshot is inlined into a single
bash -c "<string>"command line, which on Windows is subject to the ~8191-charcmd.exelimit. 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; npmlatestis 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-officialmarketplace and a local marketplace) cmd.exe-launched Claude processes are also present (via a bridge tool that spawnscmd /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/statSyncanywhere in this path — the directory does not have to exist. -
HEis 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):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
- Windows 11 + Git Bash, enable ~25 plugins that ship no
bin/directory (most do not). echo $PATH | tr ':' '\n' | grep -c plugins→ one dead entry per enabled plugin.- 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).
ls -t ~/.claude/shell-snapshots/*.sh | head -1→ the new snapshot is ~7170 B with a doubled plugin block.- Any Bash call then fails with
unexpected EOF while looking for matching \'`` once PATH grows slightly more.
Suggested fixes
- 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. - Do not skip the metacharacter filter on Windows based only on the separator — the current
HE !== "\\"guard disables it for every Windows path. - 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 -cstring over the command-line limit. - Validate the composed snapshot/script before use, or write it atomically, so a truncated/partial snapshot surfaces as an error instead of as
unexpected EOFin every subsequent call. - 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 |
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by locating the plugin bin PATH builder corresponding to the de-minified dVn function, then inspect how its result is inserted into the shell snapshot. Reproduce on Windows with the latest shell snapshot under ~/.claude/shell-snapshots/ and verify the command-line-length failure. Done means nonexistent plugin bin directories are excluded and generated snapshots remain valid below the Windows limit.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- shell, typescript
- Domain
- cli, devtools, operating-systems
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100