anthropics / anthropics/claude-code
[BUG] Plugin update leaves a truncated version dir in the plugin cache (81 of 108 skills missing) when the same plugin@version is installed at user scope and project scope
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## Summary
After a plugin auto-update, the versioned cache directory under `~/.claude/plugins/cache////` contained only a subset of the plugin's files. 27 of 108 `skills/*` directories and the whole of one other top-level directory were missing, while `agents/`, `hooks/`, `scripts/`, `.mcp.json`, `plugin.json` and `README.md` were copied completely. The marketplace clone under `~/.claude/plugins/marketplaces//` was complete and at the expected commit, so the loss happened during the marketplace → cache copy, not during the git fetch.
Sessions then loaded the plugin without error and simply exposed 27 skills instead of 108. There was no diagnostic anywhere: no warning at startup, `/reload-plugins` reported the reduced count as if it were correct, and slash-command autocomplete just lacked the missing skills.
The same plugin's cache dirs for the four previous versions are all complete. One much older version dir on the same machine is also truncated (60 skills), so this has happened at least twice.
## Environment
- Claude Code 2.1.272, macOS (Darwin 25.6.0), Claude Desktop "Code" tab and CLI sessions on the same machine
- Plugin from a GitHub-sourced marketplace (`source: github`), plugin path `./plugins/` inside the marketplace repo
- Plugin size: 108 skills, 45 agents, ~900 files
- The plugin is installed **both at user scope and at project scope** (several projects, including git worktrees), all entries pointing at the **same** `installPath`
## What `installed_plugins.json` shows
Two entries for the same `plugin@marketplace`, same `version`, same `installPath`, `lastUpdated` **2 seconds apart**:
```json
{
"scope": "project",
"projectPath": "/path/to/project-a",
"installPath": "~/.claude/plugins/cache///0.27.6",
"version": "0.27.6",
"lastUpdated": "2026-09-15T09:42:22.182Z"
},
{
"scope": "user",
"installPath": "~/.claude/plugins/cache///0.27.6",
"version": "0.27.6",
"lastUpdated": "2026-09-15T09:42:24.435Z"
}
```
The cache dir's mtime matches these timestamps. Both updates appear to have (re)written the same directory within the same two-second window.
## Evidence
```
$ ls ~/.claude/plugins/marketplaces//plugins//skills | wc -l
108
$ ls ~/.claude/plugins/cache///0.27.6/skills | wc -l
27
$ ls ~/.claude/plugins/cache///0.27.5/skills | wc -l
108
$ diff <(cd marketplaces/.../ && find . -type f | sort) \
<(cd cache/.../0.27.6 && find . -type f -not -path './.in_use/*' | sort) | grep -c '^<'
340
```
Missing files broke down as 82 under `skills/` and 12 under one other directory; no files were missing from `agents/`, `hooks/`, `scripts/`, or the plugin root. The surviving 27 skill dirs are not alphabetically contiguous, so this is not a simple "copy stopped at entry N".
The marketplace clone was at the commit recorded in `gitCommitSha`, with a clean working tree.
## Hypothesis
Two install/update code paths (user scope and project scope, or two sessions triggering auto-update) ran concurrently against the same `` directory. One removed and recreated the directory while the other was mid-copy, so the final tree is whichever subset of the second copy landed after the first one's delete. The per-directory pattern (some top-level dirs complete, `skills/` two-thirds empty) is consistent with an `rm -rf` racing an ongoing recursive copy.
I cannot confirm this from source; it is the only explanation I found that fits the timestamps and the file pattern.
## Expected behaviour
- The versioned cache dir should either be complete or not exist. Copy into a temp dir and atomically rename into place, and/or hold a lock per `/` during install/update.
- If multiple scopes resolve to the same `installPath`, updating one should not rewrite the dir while another session may be copying into it.
- After copying, verify the result against the source (file count or manifest) and fail loudly rather than registering a partial install.
- At session start, if the resolved install dir is missing components the manifest or source declares, emit a warning instead of silently loading fewer skills.
## Workaround
Refill the cache dir from the marketplace clone without touching existing files, then `/reload-plugins`:
```bash
rsync -a --ignore-existing \
~/.claude/plugins/marketplaces//plugins// \
~/.claude/plugins/cache////
```
Deleting the version dir and reinstalling also works once no session holds an `.in_use` lease. To avoid recurrence I am consolidating to a single (user-scope) install so no two scopes share one `installPath`.
## Related
- #85278 — duplicate `installed_plugins.json` entries from multiple worktrees (registry side of the same multi-scope fragility)
- #83168 — plugin agents intermittently not registered while skills load (different symptom, similarly silent)
- #82145 — `.in_use` leases never reaped (blocks the "delete and reinstall" workaround)
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by tracing the plugin install/update paths that write the versioned cache directory under ~/.claude/plugins/cache and update installed_plugins.json, using the shared user- and project-scope installPath as the reproduction case. Verify concurrent updates cannot leave a partial tree, and confirm the completed install is validated or reported as invalid before plugins load.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100