Claude Code gates plugin updates on .claude-plugin/plugin.json's version field, which CI bumps on release. A host that has not picked up the new version sits on an old tag and reports nothing wrong.
bin/doctor checks dangling links, links resolving into a foreign checkout, count mismatches, stray entries, and leftover Claude symlinks. All of those are symlink-path concerns. The plugin path — which is the only channel for Claude Code on all three hosts — has no version check at all.
Three hosts (koala, flamingo, iguana) means three independent chances to skew, and the failure is silent by construction: the skills all load, they are just the wrong generation.
Action
bin/doctor reports the installed plugin version against the latest tag on main, and exits non-zero on mismatch (consistent with its existing contract).
Emit a machine-readable line so per-host results can be collected later without re-parsing prose.
Confirm behaviour when the plugin is not installed at all on a host that only uses the symlink path — that is a valid configuration and must not fail.
Why it is worth doing now
doctor already runs at session start via the plugin, so the detection point exists and costs nothing extra. The gap is only what it looks at.
Note the ordering trap: doctor ships inside the plugin. A host stale enough to matter is running the old doctor, which does not have this check. The first detection after this lands therefore requires one manual task update per host — worth stating in the README so it is not mistaken for the check not working.
Risk
Low. Read-only reporting, no wiring changes.
Definition of done
task doctor on a host pinned to an older tag exits non-zero and names both versions.
## Problem
Claude Code gates plugin updates on `.claude-plugin/plugin.json`'s `version` field, which CI bumps on release. A host that has not picked up the new version sits on an old tag and **reports nothing wrong**.
`bin/doctor` checks dangling links, links resolving into a foreign checkout, count mismatches, stray entries, and leftover Claude symlinks. All of those are symlink-path concerns. The plugin path — which is the only channel for Claude Code on all three hosts — has no version check at all.
Three hosts (koala, flamingo, iguana) means three independent chances to skew, and the failure is silent by construction: the skills all load, they are just the wrong generation.
## Action
- [ ] `bin/doctor` reports the installed plugin version against the latest tag on `main`, and exits non-zero on mismatch (consistent with its existing contract).
- [ ] Emit a machine-readable line so per-host results can be collected later without re-parsing prose.
- [ ] Confirm behaviour when the plugin is not installed at all on a host that only uses the symlink path — that is a valid configuration and must not fail.
## Why it is worth doing now
`doctor` already runs at session start via the plugin, so the detection point exists and costs nothing extra. The gap is only what it looks at.
Note the ordering trap: doctor ships *inside* the plugin. A host stale enough to matter is running the old doctor, which does not have this check. The first detection after this lands therefore requires one manual `task update` per host — worth stating in the README so it is not mistaken for the check not working.
## Risk
Low. Read-only reporting, no wiring changes.
## Definition of done
`task doctor` on a host pinned to an older tag exits non-zero and names both versions.
Shipped in a2afb35, released as v0.9.0 — CI run #31conclusion: success.
All three checkboxes, plus the live proof this was not hypothetical.
Reports installed vs available, exits non-zero. doctor reads the version recorded for skills@mathias in installed_plugins.json and compares it against the newest manifest already on the host — the active checkout and any marketplace clone, filtered to plugin.json files that actually name this plugin (the caveman marketplace has one too). Offline, like everything else in doctor: "available" never means a fetch.
Machine-readable.--json grew a plugin object alongside findings:
Not-installed is not a fault. A host with no install record is a valid symlink-only configuration and stays clean — covered by a fixture.
It caught koala immediately
Writing the check found the exact state this issue predicted, on the machine I was working from:
FAIL plugin-version: Claude Code has skills@mathias 0.6.0 installed but 0.8.4
is available on this host — /plugin update skills@mathias
Installed 2026-08-23, last updated 08-24, and never since. Three releases behind at the time, and task doctor had been saying clean — 42 skills throughout. The count was right; the generation was not.
The ordering trap you flagged is real and is now in the failure message itself, since a host stale enough to matter is running the old doctor and will not print this until someone updates it by hand once.
Four fixture cases: version match, stale install, absent install record, and a marketplace clone newer than the checkout (an update waiting even when the local checkout agrees with what is installed).
Shipped in `a2afb35`, released as **v0.9.0** — CI run [#31](https://git.d-ma.be/mathias/skills/actions/runs/4841) `conclusion: success`.
All three checkboxes, plus the live proof this was not hypothetical.
**Reports installed vs available, exits non-zero.** doctor reads the version recorded for `skills@mathias` in `installed_plugins.json` and compares it against the newest manifest already on the host — the active checkout and any marketplace clone, filtered to `plugin.json` files that actually name this plugin (the caveman marketplace has one too). Offline, like everything else in doctor: "available" never means a fetch.
**Machine-readable.** `--json` grew a `plugin` object alongside `findings`:
```json
{"checkout":"/home/mathias/dev/skills","expected":42,
"plugin":{"installed":"0.6.0","available":"0.8.4"},"findings":[...]}
```
**Not-installed is not a fault.** A host with no install record is a valid symlink-only configuration and stays clean — covered by a fixture.
## It caught koala immediately
Writing the check found the exact state this issue predicted, on the machine I was working from:
```
FAIL plugin-version: Claude Code has skills@mathias 0.6.0 installed but 0.8.4
is available on this host — /plugin update skills@mathias
```
Installed 2026-08-23, last updated 08-24, and never since. Three releases behind at the time, and `task doctor` had been saying `clean — 42 skills` throughout. The count was right; the generation was not.
The ordering trap you flagged is real and is now in the failure message itself, since a host stale enough to matter is running the old doctor and will not print this until someone updates it by hand once.
Four fixture cases: version match, stale install, absent install record, and a marketplace clone newer than the checkout (an update waiting even when the local checkout agrees with what is installed).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Claude Code gates plugin updates on
.claude-plugin/plugin.json'sversionfield, which CI bumps on release. A host that has not picked up the new version sits on an old tag and reports nothing wrong.bin/doctorchecks dangling links, links resolving into a foreign checkout, count mismatches, stray entries, and leftover Claude symlinks. All of those are symlink-path concerns. The plugin path — which is the only channel for Claude Code on all three hosts — has no version check at all.Three hosts (koala, flamingo, iguana) means three independent chances to skew, and the failure is silent by construction: the skills all load, they are just the wrong generation.
Action
bin/doctorreports the installed plugin version against the latest tag onmain, and exits non-zero on mismatch (consistent with its existing contract).Why it is worth doing now
doctoralready runs at session start via the plugin, so the detection point exists and costs nothing extra. The gap is only what it looks at.Note the ordering trap: doctor ships inside the plugin. A host stale enough to matter is running the old doctor, which does not have this check. The first detection after this lands therefore requires one manual
task updateper host — worth stating in the README so it is not mistaken for the check not working.Risk
Low. Read-only reporting, no wiring changes.
Definition of done
task doctoron a host pinned to an older tag exits non-zero and names both versions.Shipped in
a2afb35, released as v0.9.0 — CI run #31conclusion: success.All three checkboxes, plus the live proof this was not hypothetical.
Reports installed vs available, exits non-zero. doctor reads the version recorded for
skills@mathiasininstalled_plugins.jsonand compares it against the newest manifest already on the host — the active checkout and any marketplace clone, filtered toplugin.jsonfiles that actually name this plugin (the caveman marketplace has one too). Offline, like everything else in doctor: "available" never means a fetch.Machine-readable.
--jsongrew apluginobject alongsidefindings:Not-installed is not a fault. A host with no install record is a valid symlink-only configuration and stays clean — covered by a fixture.
It caught koala immediately
Writing the check found the exact state this issue predicted, on the machine I was working from:
Installed 2026-08-23, last updated 08-24, and never since. Three releases behind at the time, and
task doctorhad been sayingclean — 42 skillsthroughout. The count was right; the generation was not.The ordering trap you flagged is real and is now in the failure message itself, since a host stale enough to matter is running the old doctor and will not print this until someone updates it by hand once.
Four fixture cases: version match, stale install, absent install record, and a marketplace clone newer than the checkout (an update waiting even when the local checkout agrees with what is installed).