Marketplace clone's fetch refspec is pinned to the ref kind at clone time: changing a marketplace `ref` from a tag to a branch freezes the clone permanently, and `marketplace update` still reports success
Bug
A git-sourced marketplace clone is created with a fetch refspec derived from the kind of ref that source.ref resolved to at clone time. If the configured ref later stops resolving under that refspec — for example because you moved from a moving tag to a branch of the same name — the refspec matches nothing, the clone is permanently frozen at whatever commit it holds, and claude plugin marketplace update <name> keeps reporting success.
Nothing anywhere reports a problem. The catalog silently serves an old plugin.json forever, so every consumer stays on an old plugin version.
Environment
Claude Code 2.1.228, Windows (win32 10.0.26100). Private GitHub marketplace declared through extraKnownMarketplaces with a pinned ref.
What we did
We used a moving annotated tag as the "released" pointer (ref: <name>), re-pointing it on each release. We then decided a branch was a better fit, so we deleted the remote tag and created a branch with the same name, pointing at the same commit.
Observed
The existing clone had been created while <name> was a tag:
$ git -C ~/.claude/plugins/marketplaces/<name> config --get remote.origin.fetch
+refs/tags/<name>:refs/tags/<name>
After the remote tag was deleted and replaced by a branch of the same name:
remote: refs/heads/<name> -> <new commit> (0.9.0)
clone: HEAD -> <old commit> (0.6.0)
$ claude plugin marketplace update <name>
✔ Successfully updated marketplace: <name>
$ # catalog plugin.json still reports 0.6.0, HEAD still at the old commit
Repeating the update never helps. The refspec cannot match anything, because refs/tags/<name> no longer exists on the remote.
Two smaller observations from the same episode:
- While both existed, the stale local tag shadowed the new branch: git resolves
refs/tags/beforerefs/heads/, sogit -C <clone> rev-parse <name>returned the old tag object even though a branch of that name existed on the remote. Deleting only the local tag is not enough either — the refspec is still tag-shaped, so subsequent fetches bring nothing. - A deleted remote tag is not pruned from the clone by
marketplace update, so the stale ref lingers indefinitely.
Repro
- Create a marketplace repo. Tag a commit
rel(annotated) and declare{"source": {"source": "github", "repo": "...", "ref": "rel"}}. - Install a plugin from it, confirm
remote.origin.fetchis+refs/tags/rel:refs/tags/rel. - Delete the remote tag
rel; create a branchrelat a newer commit that bumps the pluginversion. claude plugin marketplace update <name>→ reports success, changes nothing. New sessions keep loading the old version.
Only working fix
Delete the clone and let it be recreated:
rm -rf ~/.claude/plugins/marketplaces/<name>
claude plugin marketplace update <name>
The fresh clone gets a branch-shaped refspec and immediately reaches the new commit:
+refs/heads/<name>:refs/remotes/origin/<name>
Installed plugin copies live under ~/.claude/plugins/cache/, a separate directory, so removing the marketplace clone does not disturb them. (claude plugin marketplace remove is not usable here — it uninstalls the plugins.)
Expected
Any of these would have prevented an unrecoverable-looking state:
- Re-derive the refspec (or recreate the clone) when the configured
refno longer resolves under the stored refspec. - Have
marketplace updatefail loudly when the configuredrefcannot be resolved on the remote, instead of printing success. - Prune deleted remote refs on update, and prefer
refs/heads/<ref>over a localrefs/tags/<ref>when both could match a configuredref.
The reporting behaviour is the more damaging half: a command that prints ✔ Successfully updated while doing nothing gives no reason to look further, and the resulting staleness has no other symptom.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗