[BUG] Interrupted plugin cache copy leaves a partial version directory that is later installed as "already cached"

Status Open
Reported on v2.1.300
Maintainer reply None cached
Activity 0 comments · opened Oct 14, 2026

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 under data/; no .claude-plugin/, no skills/.
  • 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 -rf of the directory, then claude plugin update demo@repro: 6007 entries, and plugin details shows demo 0.1.0 and Skills (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.

View original on GitHub ↗