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

Open
#94,547 0 comments 0 reactions 0 assignees View on GitHub
area:plugins bug has repro platform:macos
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.