[BUG] Interrupted plugin cache copy leaves a partial version directory that is later installed as "already cached"
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
If a copy into ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ is interrupted, the incomplete directory stays. The next claude plugin update, or the next session's background re-run of a command source, finds that directory, installs it as the new version, and reports success. The plugin then loads with components missing and no warning: claude plugin list shows it enabled, and claude plugin details shows Skills (0).
claude plugin update (already at the latest version) and claude plugin uninstall + claude plugin install leave the partial directory in place. Deleting the directory and running claude plugin update restores it.
Also observed: a claude -p session that ended normally while its once-per-session background run of a command source was copying (the debug log's last line was Copying plugin demo@repro to versioned cache (fallback to full copy)) left the same kind of partial directory, and a later session installed it.
Impact: a plugin silently loses some or all of its skills, hooks or other components until the user finds and deletes the cache directory by hand.
What Should Happen?
A version directory in the plugin cache is either complete or absent, so an interrupted copy is redone on the next install, update or session. Failing that, Claude Code should not install or load a directory it cannot show is complete, and should report it.
Error Messages/Logs
No warning or error about the plugin, at any log level. From the session path above: debug log of a later session, after a claude -p session had exited during the copy (paths shortened):
[DEBUG] Using content-hash version for command-sourced demo@repro: 0.1.0-1851f9188243
[DEBUG] Plugin demo@repro version 0.1.0-1851f9188243 already cached at ~/.claude/plugins/cache/repro/demo/0.1.0-1851f9188243
[DEBUG] Updated demo@repro on disk to version 0.1.0-1851f9188243 at ~/.claude/plugins/cache/repro/demo/0.1.0-1851f9188243
[DEBUG] Plugin autoupdate: updated demo@repro from 0.1.0-edc85643fbb9 to 0.1.0-1851f9188243
and of the session after that:
[DEBUG] Checking plugin demo: skillsPath=none, skillsPaths=0 paths
Steps to Reproduce
Needs no API calls. gen.sh writes 6000 filler files so the copy takes long enough to interrupt.
R=$(mktemp -d)
mkdir -p "$R/mkt/.claude-plugin"
cat > "$R/gen.sh" <<'GEN'
#!/bin/sh
# Builds a plugin directory whose content depends on rev.txt and prints its path.
set -eu
here=$(dirname "$0")
rev=$(cat "$here/rev.txt")
out="$here/out/$rev"
if [ ! -d "$out" ]; then
mkdir -p "$out/.claude-plugin" "$out/skills/hello" "$out/data"
printf '{"name":"demo","version":"0.1.0"}\n' > "$out/.claude-plugin/plugin.json"
printf -- '---\nname: hello\ndescription: Say hello\n---\nSay hello.\n' > "$out/skills/hello/SKILL.md"
i=0; while [ $i -lt 6000 ]; do echo "$rev $i" > "$out/data/f$i.txt"; i=$((i+1)); done
fi
echo "$out"
GEN
cat > "$R/mkt/.claude-plugin/marketplace.json" <<JSON
{
"name": "repro",
"owner": { "name": "repro" },
"plugins": [
{ "name": "demo", "source": { "source": "command", "command": "sh $R/gen.sh" } }
]
}
JSON
echo r1 > "$R/rev.txt"
claude plugin marketplace add "$R/mkt"
claude plugin install demo@repro -y
# New content, then interrupt the update while it copies into the cache.
echo r2 > "$R/rev.txt"; sh "$R/gen.sh" >/dev/null
C=${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache/repro/demo
n0=$(ls "$C" | wc -l)
claude plugin update demo@repro &
pid=$!
until [ "$(ls "$C" | wc -l)" -gt "$n0" ]; do sleep 0.05; done
sleep 0.1; kill -TERM "$pid"; wait "$pid"
ls -A "$C"/*/ # the new version dir holds only part of data/
claude plugin update demo@repro # reports success and installs the partial dir
claude plugin list
claude plugin details demo
Observed (2.1.300):
- The interrupted update printed only
Checking for updates for plugin "demo@repro"…. The new directory held 724 of the 6007 entries, all underdata/; no.claude-plugin/, noskills/. - The second update printed
✔ Plugin "demo" updated from 0.1.0-844d7c2dd5f2 to 0.1.0-4380c7d79f2b for scope user.and left the directory unchanged. claude plugin list:Status: ✔ enabled.claude plugin details demo:Skills (0), with no version line.rm -rfof the directory, thenclaude plugin update demo@repro: 6007 entries, andplugin detailsshowsdemo 0.1.0andSkills (1) hello.
Interrupting with SIGINT instead of SIGTERM gave the same result.
Claude Model
N/A (the repro makes no API calls)
Is this a regression?
I don't know
Claude Code Version
2.1.300 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu 24.04 (WSL2)
Terminal/Shell
Non-interactive (scripted sh)
Additional Information
#94547 reports the same end state (a version directory missing most of a plugin's skills, loaded without warning) with a different suspected trigger: two scopes updating one directory at once.