anthropics / anthropics/claude-code
[BUG] Auto-update runs for disabled plugins and re-clones source:"url" repos before comparing versions
- Lenguaje dominante
- Python
- Estrellas
- 145k
- Forks
- 23.1k
- Métricas de merge de PR
- Métricas de PR pendientes
Descripción
### What's Wrong?
**Differential first, since the plugin cache has several open issues and this is none of them:**
- **Not #90781** — that reports `temp_git_*` clone-staging dirs surviving and never being swept. This reports what *creates* them in the volume it does. #90781 is the leak; this is the tap. Fixing either alone leaves the other.
- **Not #86139 / #84401 / #89243 / #91271** — those are all about auto-update being silently *off*, not pulling, or losing `autoUpdate`. This is the opposite direction: updates running for plugins that should be excluded.
- **#22461** (closed as duplicate, Feb 2026) described "marketplace plugins re-fetched repeatedly despite valid cached version" — same family, apparently never root-caused. This report supplies a mechanism.
Observed on **2.1.271 and 2.1.272**, macOS.
---
### Two defects that compound
**1. Auto-update does not filter on `enabledPlugins`.**
A plugin disabled in `settings.json` is still resolved and updated. Directly checkable — `settings.json`:
```json
"enabledPlugins": { "hyperframes@claude-plugins-official": false }
```
and `installed_plugins.json` for that same plugin:
```json
"hyperframes@claude-plugins-official": [{
"scope": "user",
"installPath": ".../cache/claude-plugins-official/hyperframes/0.8.38",
"version": "0.8.38",
"installedAt": "2026-06-17T03:52:59.124Z",
"lastUpdated": "2026-09-14T22:30:10.100Z"
}]
```
Disabled since June; updated in September. The plugin has never been invoked once.
This looks like an omission rather than a decision: the command-source refresher in the same chunk *does* check, and says so — the binary contains the literal `Plugin autoupdate: not re-resolving ` + ` (disabled or blocked by policy)`. The regular pass has no equivalent. It does read the enabled/disabled sets, but feeds them to the pinner-constraint lookup (`disabledPinners`, `autoupdate-blocked-by-pinner`), not to a skip.
**2. `source:"url"` plugins are cloned *before* the version is compared.**
Early `up_to_date` returns exist for `claudeai` sources, the archive/entry-helper path, and tag-constrained pinner paths (`resolvedVersion` + `gitCommitSha` compared against `ls-remote`). A plain `source:"url"` entry matches none of them, so the full clone is paid on every pass even when the installed version is already current.
Evidence on disk — two staging clones of the same repo, 86 minutes apart on the same day, **after** the plugin was already recorded at its current version:
```
temp_git_1789423887909_lebnqz 15:11 922M .git/lfs = 371M
temp_git_1789429044078_c9gob5 16:37 877M
both: git config remote.origin.url = https://github.com/heygen-com/hyperframes.git
```
The second produced no version change and wrote no `lastUpdated`.
---
### Why it gets expensive
- **Git-LFS smudge is on and cannot be turned off for a plugin source.** `skipLfs` exists only on *marketplace* sources — it appears in the settings schema twice, both inside the marketplace source union, and every call site is a `case "github"` / `case "git"` marketplace branch. The three git helpers that honor it (clone, pull, sparse-checkout) each merge `GIT_LFS_SKIP_SMUDGE:"1"` into the git env when it is set. The *plugin* clone helper is a different function — it is the one tagged `plugin_clone` in telemetry, versus `marketplace_clone` — and it passes its git env straight through with no LFS merge and no equivalent option. So each plugin clone pulls the full LFS payload: ~880 MB working tree, which becomes a 2.2 GB install dir once dependencies are installed.
- **Per-process scheduling.** Each Claude Code process runs the pass once, after a randomized delay of up to ten minutes (`Math.floor(Math.random()*D)` with `D = 600000`). I could not establish whether any longer-period throttle also applies — but empirically it did not prevent two full clones of the same repository 86 minutes apart on the same machine on the same day, and with many concurrent sessions the passes overlap and abandon partial clones.
- **`lastUpdated` undercounts it.** It is written only on an `updated` outcome, never `up_to_date` — so the field you would naturally check to measure this is blind to most of the traffic. The abandoned clones on disk are the honest record.
**Net cost on one machine:** a disabled, never-used, git-LFS plugin accounted for **~39 GB of a 48 GB plugin cache** — 41 leftover staging clones totalling ~26 GB, plus 6 cached version directories at 2.2 GB each, of which only one was referenced.
### Steps to Reproduce
1. Install a plugin from a marketplace with auto-update on, whose upstream is a large git-LFS repo and which ships releases frequently.
2. Disable it: `"@": false` in `settings.json`.
3. Never use it.
4. Start Claude Code sessions normally for a few weeks.
5. `du -sh ~/.claude/plugins/cache` and list `temp_*` directories; check `lastUpdated` on the disabled plugin in `installed_plugins.json`.
### Expected
A disabled plugin is not fetched. A `source:"url"` plugin whose recorded `gitCommitSha` still matches `git ls-remote` is not re-cloned.
### Suggested fix
- Add the enabled check to the regular pass so it matches the command-source refresher's existing behavior.
- For `source:"url"`, compare the recorded `gitCommitSha` against `git ls-remote` before cloning, as the tag-pinned path already does.
- Optionally, honor `skipLfs` on plugin entry sources too.
Happy to supply the function names and byte offsets I read this from, though they are build-specific; everything above is reproducible from the on-disk artifacts without them.
Guía de contribución
No hay ninguna guía de contribución indexada para este repositorio
Evaluación
Este issue todavía no se ha evaluado.