[BUG] Truncated shell snapshot silently drops plugin bin/ PATH entries (partial snapshot used after builder is killed)

Open 💬 1 comment Opened Jun 12, 2026 by yasyf

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Plugin bin/ directories reach the Bash tool's PATH solely through the final export PATH=... line of the per-session shell snapshot (<config>/shell-snapshots/snapshot-zsh-*.sh). The snapshot builder runs the user's login shell via execFile with a 10,000 ms timeout and 1 MB maxBuffer. When the builder is killed, the partially written snapshot file is still used for the whole session: every section written before the kill works, but the PATH export — written last — is missing. Nothing validates the file before use.

The session then behaves normally in every visible way (the inherited process PATH covers normal commands), except enabled plugins' executables are silently not invokable by bare name: command not found, exit 127, for the entire session. It is intermittent — on the same machine, the same day, healthy and truncated snapshots interleave minutes apart.

What Should Happen?

Per the plugins reference: "Executables added to the Bash tool's PATH. Files here are invokable as bare commands in any Bash tool call while the plugin is enabled." A failed/killed snapshot build should not silently strip plugin bin paths — the snapshot should be written atomically (temp file + rename on success), or validated before use, or plugin bin paths should be delivered via the spawn env instead of the snapshot.

Error Messages/Logs

# In the affected session — plugin enabled, binary present and executable in the cache:
$ cc-review start --session "$CLAUDE_CODE_SESSION_ID" --cwd "$PWD"
(eval):1: command not found: cc-review        # exit 127

# Absolute path works in the same session, immediately after:
$ "$HOME/.claude/plugins/cache/cc-review/cc-review/0.10.0/bin/cc-review" start ...
http://127.0.0.1:.../s/...

# On-disk snapshots from the same machine, same day (sizes byte-exact):
# 20:33:57  203,921 B  ends with: export PATH=<base>:...:<11 plugin bin dirs>   (healthy)
# 20:40:01  201,921 B  identical, except the final export PATH line is absent   (affected session)
# 20:44-20:49 (x3)  203,9xx B  healthy
# 20:51:45   92,167 B  cut mid-way through the typeset -f function dump
# 20:56:38  201,921 B  final export PATH line absent

Steps to Reproduce

  1. Install any plugin that ships an executable in bin/ (the documented pattern), e.g. one whose skill invokes the tool by bare name.
  2. Make shell init exceed the builder's 10 s timeout — add sleep 11 to ~/.zshrc (organically this happens under system load; my zshrc normally sources in ~0.3 s and still produced the truncated snapshots above).
  3. Start a new session and run the plugin's binary by bare name in the Bash tool.
  4. Exit 127 command not found. Inspect <config>/shell-snapshots/snapshot-zsh-<id>.sh: the file exists and is sourced, but has no export PATH= line (the cut point varies with where the kill lands; when init is just under the limit, everything but the last-written PATH heredoc makes it).
  5. Deterministic check of the degraded state without the timeout: delete the final export PATH= line from a healthy session's snapshot — subsequent Bash calls in that session lose bare-name access to all plugin binaries while everything else keeps working.

Is this a regression?

I don't know

Claude Code Version

2.1.175 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other (Ghostty; snapshots are zsh)

Additional Information

From the 2.1.175 bundle, for whoever picks this up:

  • getEnabledPluginBinPaths maps every enabled non-builtin plugin to join(plugin.path, "bin") (no existence check; shell-metachar filter only) — this list reaches the Bash tool only via the snapshot's final PATH heredoc.
  • The builder appends sections in order (functions, aliases, rg check, optional bq shadow, PATH heredoc last) and runs via execFile(shell, ["-c","-l", script], { timeout: 10000, maxBuffer: 1048576 }).
  • On error the failure is debug-logged and a telemetry event fires, but the partial snapshot remains in place and the session uses it.

Suggested fixes, any one of which would close the hole: (1) write to a temp file and rename only on success; (2) validate a completeness sentinel (or the PATH line) before use and rebuild on failure; (3) compose plugin bin paths into the spawn env of each Bash invocation instead of the snapshot; (4) make the timeout configurable for slow shell inits.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗