[BUG] plugin update / auto-update compare only the version string, so plugins that ship new content without bumping version never refresh
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
claude plugin update and marketplace auto-update decide whether an installed plugin is out of date by comparing the version string alone. They never look at the resolved source commit. So a plugin whose author publishes new content on main without bumping plugin.json version is never refreshed, even though claude plugin marketplace update has already fetched the new commits into the marketplace clone and the marketplace has autoUpdate: true.
Concrete case: langsmith-skills@langsmith-skills (marketplace langchain-ai/langsmith-skills). Its plugin.json has said "version": "0.1.0" since the manifest was added on 2026-03-10 and has never changed. Since then the repo has landed 5 commits touching config/skills/ (the last one, e8f4120 on 2026-08-17, rewrote the langsmith-evaluator skill for the current LangSmith CLI). On 2026-08-24 the installed plugin cache was still the content from the original install, four months behind the marketplace clone sitting next to it on disk. claude plugin update langsmith-skills@langsmith-skills reported "already at the latest version (0.1.0)". The marketplace entry had autoUpdate: true in known_marketplaces.json; auto-update never re-pulled it either.
The only way to get the new content is claude plugin uninstall followed by claude plugin install.
The same comparison also affects the official marketplace's own source: {url, sha} pinned entries (e.g. superpowers, firecrawl): when the pinned sha moves but the plugin's version does not, plugin update reports "already at the latest version". I could not confirm the installed content was stale in that case because gitCommitSha in installed_plugins.json is itself unreliable (#86194), so I report it as a consequence of the same logic rather than an independently verified repro.
What Should Happen?
Freshness should be decided against the resolved source, not only the version string. Either:
- For
github/gitsources: compare the installed commit against the marketplace clone's current HEAD for the plugin's path; forurlsources with ashapin: compare against the pin. Update when they differ, even ifversionis unchanged. Or, - At minimum, add
claude plugin update --force(re-fetch and reinstall regardless of version) so users are not left with uninstall + reinstall as the workaround.
Marketplaces with autoUpdate: true should apply the same rule, since that flag currently implies a guarantee it does not deliver for unversioned plugins.
Error Messages/Logs
$ claude plugin marketplace update
Updating 3 marketplace(s)...✔ Successfully updated 3 marketplace(s)
$ git -C ~/.claude/plugins/marketplaces/langsmith-skills rev-parse --short HEAD
e8f4120
$ claude plugin update langsmith-skills@langsmith-skills
Checking for updates for plugin "langsmith-skills@langsmith-skills" at user scope…
✔ langsmith-skills is already at the latest version (0.1.0).
$ diff -rq ~/.claude/plugins/cache/langsmith-skills/langsmith-skills/0.1.0/config/skills \
~/.claude/plugins/marketplaces/langsmith-skills/config/skills
(differences listed: skill content in the cache is months older than the clone)
$ claude plugin uninstall langsmith-skills@langsmith-skills --scope user && \
claude plugin install langsmith-skills@langsmith-skills --scope user
(diff is now empty)
Steps to Reproduce
claude plugin marketplace add langchain-ai/langsmith-skillsandclaude plugin install langsmith-skills@langsmith-skills --scope userat some commit of that repo (or any marketplace whose plugin keeps a fixedversion).- Wait for (or create, with a local marketplace) new commits to the plugin's skill files with
plugin.jsonversionunchanged. claude plugin marketplace update. Confirm the marketplace clone HEAD advanced.claude plugin update <plugin>@<marketplace>. Observe "already at the latest version".diff -rqthe installed cache dir against the marketplace clone. Observe the cache is still the old content.- Uninstall + install. Observe the diff is now empty.
Claude Model
Not model related.
Is this a regression?
No, as far as I can tell. The docs describe version as the update authority; this report argues that rule leaves unversioned and sha-pinned plugins permanently stale.
Last Working Version
N/A
Claude Code Version
2.1.226
Platform
macOS
Operating System
macOS (Darwin 25.6.0)
Terminal/Shell
zsh, Claude desktop app (Code tab) and CLI
Additional Information
Related but distinct: #86194 (gitCommitSha metadata not updated after a successful update), #86700 (plugin install sees a newer version but does not upgrade), #79776 (versionless official plugins re-trigger the updated banner). None of them cover the case where no version bump ever happens and content silently stays stale.
3 Comments
Another instance with the same shape, on a first-party plugin:
cloudflare@cloudflarefrom the
cloudflare/skillsmarketplace..claude-plugin/plugin.jsonhas said"version": "1.0.0"since it was added and hasnever been edited. The
marketplace.jsonentry declares no version at all, so thecomparison has nothing to work with from either side.
Installed 2026-03-17 at
d311303. Measured 2026-09-10: the cache was still servingskills dated 2026-03-16, while the clone sitting next to it was current to
b052c32(2026-09-07). 87 merged PRs and 219 commits landed in between.
autoUpdate: truewasset the whole time, and
claude plugin marketplace updatehad been refreshing the clonewithout ever touching the cache.
One consequence here goes past stale docs. The plugin's
.mcp.jsonchanged too: it nowdeclares a single
https://mcp.cloudflare.com/mcp, but my session was still connectingto five server names Cloudflare has since retired (
cloudflare-api,-bindings,-builds,-docs,-observability). A frozen version string can leave users pointed atendpoints that no longer exist, not only reading old guidance.
Uninstall plus install was the only fix here as well. I filed cloudflare/skills#196 for
the version bump on their side, but as you say, the client-side comparison is the general
problem.
One more case, measured, where
claude plugin updateis worse than "never refreshes": it can print a successful update while serving stale files, when the new version string was already installed once on the machine.Setup: Claude Code 2.1.236 on Windows 11, a throwaway local-directory marketplace with a relative
sourceentry,versionset only inplugin.json. For each case I changed a skill file and the version, ranclaude plugin marketplace update <mkt>thenclaude plugin update <plugin>@<mkt>, and compared a hash of the installed tree with the published one.| Case |
plugin updateoutput | Installed files ||---|---|---|
| content changed, version unchanged (0.1.0) |
is already at the latest version (0.1.0)| stale || 0.1.0 → 0.1.1 · 0.1.9 → 0.1.10 · 1.0 → 1.0.0 |
updated from … to …| fresh || downgrade to a version never installed here (0.1.10 → 0.0.5) |
updated from 0.1.10 to 0.0.5| fresh || downgrade to a version installed earlier (0.1.1 → 0.1.0) |
updated from 0.1.1 to 0.1.0| stale: the files of the first 0.1.0 install || back to a version installed earlier (0.1.0 → 0.1.1, e.g. after a revert) |
updated from 0.1.0 to 0.1.1| stale: the earlier 0.1.1 files ||
plugin uninstallthenplugin install, same version |Successfully installed plugin| fresh |What this shows:
1.0≠1.0.0.~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/,updatereuses that directory as-is instead of copying the source again, and still reportsupdated. Superseded version directories are not deleted on update: they only get an.orphaned_atmarker, and the bundled CLI appears to sweep them 14 days later, so a version string that comes back within that window is served from the old copy.installstages a fresh copy of the source, which is why uninstall + install fixes both this case and the one in the original report.From the bundled code, the update path only asks the cache step to overwrite when the resolved version is
unknown; otherwise an existing non-empty version directory is returned untouched. The same logic is still there in 2.1.266, the CLI bundled with the desktop app.In addition to
--force: when the target version's cache directory already exists,updateshould re-copy it or check it against the source before reusing it, and it should not reportupdatedwhen the files it serves have not changed.One reading that may sharpen the mechanism section of this report: the stale install is
detectable from the CLI's own record, and the CLI does not look at the field that would detect it.
Two identity fields, one comparator. On 2.1.228, an
installed_plugins.jsonentry carriedversion: 0.0.0-dev(a constant from the plugin manifest) and a valid 40-charactergitCommitShasimultaneously.claude plugin updatecomparedversiononly —✔ <plugin> is, exit 0, nothing delivered — while the installed contentalready at the latest version (0.0.0-dev)
was months behind the marketplace clone on the same disk and the
gitCommitShabeside the constantnamed the commit the install had actually come from. The datum that would prove the install stale is
already in the record; it is just not the one the comparator reads.
Controlled reading of what the
versionkey changes. Two arms, identical except for the pluginmanifest's
versionkey; marketplace entry keyless in both; cleanHOMEper arm; same CLI.| Arm |
plugin.jsonversion|gitCommitSharecorded |versionrecorded ||---|---|---|---|
| with-version |
"0.0.0-dev"| yes |0.0.0-dev|| keyless | absent | yes | a 12-character commit string |
So the key does not suppress
gitCommitSha; it decides what lands inversion. Keyless manifestsget a commit-derived string there, which is why they update, and versioned manifests get a constant,
which is why they do not. That is consistent with the
langsmith-skillsandcloudflarecasesreported above — as described in those reports, both carry a constant
versionwhile the cloneadvances.
plugin list --jsoncannot detect it either. Its output is a projection ofinstalled_plugins.json: editing that file'sversionandinstallPathto sentinels changed theplugin list --jsonoutput verbatim, and restoring the file reverted it. There is no independentread of installed content to compare against, so a consumer cannot script around the comparator from
the CLI surface.
On the
gitCommitSha-is-unreliable caveat in the report above: #86194 is scoped tourl-sourcemarketplace entries and is now closed. The reading here is on a
githubsource, where the fieldtracked the delivered commit correctly in every arm measured — so it is usable as a freshness
signal for exactly the source class option 1 names, which is the narrower claim.
Smallest fix consistent with the record: when a
gitCommitShais present for agithub/gitsource, compare it against the resolved source commit and update on difference regardless of
version. That uses a field the CLI already writes, and it makesautoUpdate: truemean what itsays for unversioned plugins. The
--forcefallback proposed above would also close the workaroundgap.
Earlier evidence from the same measurements, posted before 76882 was closed:
https://github.com/anthropics/claude-code/issues/76882#issuecomment-5273479508