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

Open
#95,653 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:bash area:plugins bug has repro platform:windows
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 \'`":

  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 (NiFi, Hcka, HESh):

    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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.