doctor blames the Claude Code plugin for drift when run from a dev clone #40

Open
opened 2026-08-29 13:55:04 +00:00 by mathias · 0 comments
Owner

Found while working out the fleet rollout procedure.

Symptom

Running bin/doctor from a development clone reports:

FAIL drift: Claude Code plugin serves 44 skills, the other harnesses see 41 —
the two copies have diverged

No plugin was involved. I ran ./bin/doctor from ~/dev/skills (a working tree at v0.18.0, 44 skills) while the canonical checkout was ~/.local/share/skills at v0.12.1 with 41. The message named the Claude Code plugin, which at that moment was serving 41 like everything else.

Cause

The check compares SELF_CHECKOUT — the directory the script lives in — against CHECKOUT, and labels the former "the Claude Code plugin":

if [ -z "${DOCTOR_CHECKOUT:-}" ] && [ -f "$SELF_DIR/../.claude-plugin/plugin.json" ] \
   && [ "$SELF_CHECKOUT" != "$CHECKOUT" ]; then
  plugin_count=$(count_skills "$SELF_CHECKOUT/skills")
  ...
  add FAIL drift "Claude Code plugin serves $plugin_count skills, ..."

That inference holds when doctor runs as the plugin's SessionStart hook, where $0 genuinely is the plugin cache copy. It is wrong for every other invocation — and ~/dev/skills is exactly where someone iterating on this repo runs it from. Every skills repo has a .claude-plugin/plugin.json, so the guard does not discriminate.

Why it matters more than a cosmetic mislabel

It is a false positive that looks like a real one, in a tool whose entire contract is that a finding means something. The suggested reading — "the plugin and the symlinks have diverged" — sends you to reinstall a plugin that is fine. Doctor has spent this session earning the right to be believed; findings that are wrong about their own subject spend that down.

It also masks the true statement. What actually held was "this working clone is ahead of the canonical checkout", which is not drift at all — it is the normal state of a repo you are editing.

Fix

Name the copy by what it is, not by what it is assumed to be. Options:

  1. Detect the plugin cache path explicitly (~/.claude/plugins/cache/*/skills/*) and only claim "plugin" when $SELF_CHECKOUT is under it; otherwise report "the copy this script was run from (<path>) serves N, the canonical checkout serves M".
  2. Skip the comparison entirely when SELF_CHECKOUT is a git worktree with a different HEAD — an actively edited clone is not drift.

Option 1 is more honest and about four lines. Either way the message should print both paths, which would have made the diagnosis immediate.

Note

The adjacent plugin-version check (#30) is unaffected — it reads installed_plugins.json, so it names the real installed plugin, and it correctly reported this host has 0.12.1; origin has released 0.18.0 in the same run.

Definition of done

bin/doctor run from an arbitrary clone does not attribute that clone's contents to the Claude Code plugin, and any count-drift finding names both paths it compared.

Found while working out the fleet rollout procedure. ## Symptom Running `bin/doctor` from a development clone reports: ``` FAIL drift: Claude Code plugin serves 44 skills, the other harnesses see 41 — the two copies have diverged ``` No plugin was involved. I ran `./bin/doctor` from `~/dev/skills` (a working tree at v0.18.0, 44 skills) while the canonical checkout was `~/.local/share/skills` at v0.12.1 with 41. The message named the Claude Code plugin, which at that moment was serving 41 like everything else. ## Cause The check compares `SELF_CHECKOUT` — the directory the *script* lives in — against `CHECKOUT`, and labels the former "the Claude Code plugin": ```sh if [ -z "${DOCTOR_CHECKOUT:-}" ] && [ -f "$SELF_DIR/../.claude-plugin/plugin.json" ] \ && [ "$SELF_CHECKOUT" != "$CHECKOUT" ]; then plugin_count=$(count_skills "$SELF_CHECKOUT/skills") ... add FAIL drift "Claude Code plugin serves $plugin_count skills, ..." ``` That inference holds when doctor runs as the plugin's SessionStart hook, where `$0` genuinely is the plugin cache copy. It is wrong for every other invocation — and `~/dev/skills` is exactly where someone iterating on this repo runs it from. Every skills repo has a `.claude-plugin/plugin.json`, so the guard does not discriminate. ## Why it matters more than a cosmetic mislabel It is a **false positive that looks like a real one**, in a tool whose entire contract is that a finding means something. The suggested reading — "the plugin and the symlinks have diverged" — sends you to reinstall a plugin that is fine. Doctor has spent this session earning the right to be believed; findings that are wrong about their own subject spend that down. It also masks the true statement. What actually held was "this working clone is ahead of the canonical checkout", which is not drift at all — it is the normal state of a repo you are editing. ## Fix Name the copy by what it is, not by what it is assumed to be. Options: 1. Detect the plugin cache path explicitly (`~/.claude/plugins/cache/*/skills/*`) and only claim "plugin" when `$SELF_CHECKOUT` is under it; otherwise report "the copy this script was run from (`<path>`) serves N, the canonical checkout serves M". 2. Skip the comparison entirely when `SELF_CHECKOUT` is a git worktree with a different HEAD — an actively edited clone is not drift. Option 1 is more honest and about four lines. Either way the message should print both paths, which would have made the diagnosis immediate. ## Note The adjacent `plugin-version` check (#30) is unaffected — it reads `installed_plugins.json`, so it names the real installed plugin, and it correctly reported `this host has 0.12.1; origin has released 0.18.0` in the same run. ## Definition of done `bin/doctor` run from an arbitrary clone does not attribute that clone's contents to the Claude Code plugin, and any count-drift finding names both paths it compared.
mathias added the risk/lowsize/stype/fix labels 2026-09-16 22:22:01 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/skills#40