Summary
Three findings from a /claude-ops:plugins sync run on 2026-09-04 (Claude Code 2.1.260, claude-ops 0.41.13) against an already-current fleet. Every step was a no-op — the marketplace refreshed cleanly, update-candidates-user returned 0 of 74 user-scope ids, and missing_from_user_install and missing_from_enabled were both empty — so this is the cheapest path the algorithm has, and it is where the fixed overheads show up unmixed with real work.
None of the three are correctness bugs. The sync did the right thing. They are cost and resilience findings.
The fourth finding from the same run, stale project-scope install records, is already tracked as #3688 and has been commented there rather than duplicated here.
Finding 1 — the --ids selector rebuilds state the algorithm just built
Measured on this run:
| call |
seconds |
fleet-state.sh full JSON report |
1.46 |
fleet-state.sh --marketplace melodic-software --ids update-candidates-user |
2.26 |
claude plugin marketplace update melodic-software |
1.60 |
The selector call costs more than the full report it duplicates. It is a separate process that re-parses the 462 KB installed_plugins.json, re-walks the catalog manifests, and re-runs the realpath batch, then projects one field set out of the result.
The concurrency rule in context/sync.md already mandates a full fleet-state.sh re-read immediately before each mutating step, so on the standard path that process runs anyway and its JSON contains everything the selector emits. update-candidates-user, missing-user-install, missing-enabled, and current-project are all derivable from installed[], catalog_versions, enabled, and project_root in that same document.
The stated reason for mandating --ids over hand-rolled jq is sound and documented in context/gotchas.md: a Windows jq -r ... | while read silently appends a carriage return to every id but the last. That argues against hand-rolling the extraction — not against the script exposing the projection from an already-parsed report.
Two shapes worth considering:
fleet-state.sh --ids <selector> --from <report.json> — project from a saved report instead of rebuilding. Keeps one code path for the projection logic, so the carriage-return protection is unchanged.
- Emit the selector lists as arrays inside the main JSON report, and have the caller read them from there.
The saving is roughly 2.3 s per selector call per marketplace. On this single-marketplace run that is 2.3 s out of about 7 s of total script work. Under all mode with several marketplaces, and with Steps 2, 3, 4, and 5 each taking their own selector, it multiplies.
Finding 2 — no durable run journal, so a long sweep can lose the ability to report itself
The "Version capture for the report" section of context/sync.md is explicit about this:
Keep that snapshot (and each claude plugin update line as it is emitted) available through Step 6 rather than assuming it can be recovered; a sweep of several dozen mutations whose report depends on the old/new pairs is otherwise one context compaction away from being unable to emit its own report. This skill provides no durable log for that today.
The section names the failure and the reason the script does not write one, then leaves the mitigation as "hold it in context". On a fleet this size that is a real exposure: the pre-sweep snapshot is the sole source of every old version value, those values are destroyed by the sweep itself, and the Step 6 report format requires them. A sweep of 74 user-scope updates plus in-repo updates, interleaved with each claude plugin update output line, is exactly the shape of run most likely to hit a compaction before it reports.
The three-snapshot divergence attribution in Step 6 has the same dependency — it needs the divergences[] read from all three fleet-state.sh calls to attribute a new row to the interval it first appeared in.
Suggested fix: write a per-run journal under the plugin data directory — the pre-sweep snapshot, each mutating call and its output, and the post-sweep re-read. Step 6 then reads the journal rather than reconstructing from conversation. It also gives converge and a later audit a real before-state to compare against, and it is the durable log the CHANGELOG currently lists as deferred.
Finding 3 — Steps 4 and 5 load unconditionally on the path where they cannot do anything
The skill's docs are 92,691 bytes across 5 files. This run loaded SKILL.md (19,036 B) and context/sync.md (32,996 B) in full: about 52 KB to execute five operations, all no-ops.
context/sync.md is 508 lines. Steps 4 and 5 are roughly 100 of them and include the install_new policy branches, the --setting-sources caveat, the reinstall-recurrence caveat, the normalize-enabled-plugins.sh contract, the defaultEnabled precedence rule, and the project-scope enable-gap suppression ordering. Every one of those is unreachable when missing_from_user_install and missing_from_enabled are both empty — which, on a fleet kept current, is the common case and was the case here.
The gating signal is already in hand before either step: both arrays come from the fleet-state.sh report that Step 1 precedes and that Steps 2 and 3 already read.
Suggested fix: move Steps 4 and 5 into their own spoke, for example context/sync-install-enable.md, and have sync.md carry a one-line pointer with its read condition — read this only when missing_from_user_install or missing_from_enabled is non-empty. That is the progressive-disclosure pattern the hub already uses for converge.md and scope-semantics.md, applied to the branch of sync.md that is conditional in exactly the same way.
This trims the common path without weakening the uncommon one, and it does not touch the two blocks that SKILL.md documents as deliberate hub-only exceptions (the Report template and the install_new render).
Run evidence
Marketplace: melodic-software — current (autoUpdate: on)
lastUpdated 2026-09-04T20:23:02.759Z -> 2026-09-04T20:36:42.951Z; catalog unchanged, 0 entries and 0 versions moved
In-repo: skipped — no project context resolved from C:\Users\KyleSexton
Updated: 0 plugin(s) — update-candidates-user returned 0 of 74
Installed: 0 — missing_from_user_install empty
Enabled: 0 — missing_from_enabled empty
Divergences: 64 actionable (0 newly created by this run — 64 pre-existing) [tracked in #3688]
Stale project records: 755 record(s) across 12 path(s) [tracked in #3688]
Machine work totalled about 7 seconds; wall clock for the whole invocation was 189 seconds, and the difference is doc load and analysis rather than CLI time.
Environment
- Claude Code 2.1.260, Windows 11 Pro 10.0.26200, Git Bash
- claude-ops 0.41.13, marketplace
melodic-software, autoUpdate: on
- 74 catalog plugins, 1,079 install records,
installed_plugins.json 462,371 bytes
Verification
Timing is reproducible with date +%s.%N around each call. Two incidental notes for anyone reproducing on this machine shape: bc is not present in this Git Bash install, so the arithmetic needs awk; and the guardrails block-windows-drive-tmp guard rejects /tmp as a scratch root, so scratch files need $TEMP.
Summary
Three findings from a
/claude-ops:plugins syncrun on 2026-09-04 (Claude Code 2.1.260, claude-ops 0.41.13) against an already-current fleet. Every step was a no-op — the marketplace refreshed cleanly,update-candidates-userreturned 0 of 74 user-scope ids, andmissing_from_user_installandmissing_from_enabledwere both empty — so this is the cheapest path the algorithm has, and it is where the fixed overheads show up unmixed with real work.None of the three are correctness bugs. The sync did the right thing. They are cost and resilience findings.
The fourth finding from the same run, stale project-scope install records, is already tracked as #3688 and has been commented there rather than duplicated here.
Finding 1 — the
--idsselector rebuilds state the algorithm just builtMeasured on this run:
fleet-state.shfull JSON reportfleet-state.sh --marketplace melodic-software --ids update-candidates-userclaude plugin marketplace update melodic-softwareThe selector call costs more than the full report it duplicates. It is a separate process that re-parses the 462 KB
installed_plugins.json, re-walks the catalog manifests, and re-runs therealpathbatch, then projects one field set out of the result.The concurrency rule in
context/sync.mdalready mandates a fullfleet-state.shre-read immediately before each mutating step, so on the standard path that process runs anyway and its JSON contains everything the selector emits.update-candidates-user,missing-user-install,missing-enabled, andcurrent-projectare all derivable frominstalled[],catalog_versions,enabled, andproject_rootin that same document.The stated reason for mandating
--idsover hand-rolledjqis sound and documented incontext/gotchas.md: a Windowsjq -r ... | while readsilently appends a carriage return to every id but the last. That argues against hand-rolling the extraction — not against the script exposing the projection from an already-parsed report.Two shapes worth considering:
fleet-state.sh --ids <selector> --from <report.json>— project from a saved report instead of rebuilding. Keeps one code path for the projection logic, so the carriage-return protection is unchanged.The saving is roughly 2.3 s per selector call per marketplace. On this single-marketplace run that is 2.3 s out of about 7 s of total script work. Under
allmode with several marketplaces, and with Steps 2, 3, 4, and 5 each taking their own selector, it multiplies.Finding 2 — no durable run journal, so a long sweep can lose the ability to report itself
The "Version capture for the report" section of
context/sync.mdis explicit about this:The section names the failure and the reason the script does not write one, then leaves the mitigation as "hold it in context". On a fleet this size that is a real exposure: the pre-sweep snapshot is the sole source of every old version value, those values are destroyed by the sweep itself, and the Step 6 report format requires them. A sweep of 74 user-scope updates plus in-repo updates, interleaved with each
claude plugin updateoutput line, is exactly the shape of run most likely to hit a compaction before it reports.The three-snapshot divergence attribution in Step 6 has the same dependency — it needs the
divergences[]read from all threefleet-state.shcalls to attribute a new row to the interval it first appeared in.Suggested fix: write a per-run journal under the plugin data directory — the pre-sweep snapshot, each mutating call and its output, and the post-sweep re-read. Step 6 then reads the journal rather than reconstructing from conversation. It also gives
convergeand a lateraudita real before-state to compare against, and it is the durable log the CHANGELOG currently lists as deferred.Finding 3 — Steps 4 and 5 load unconditionally on the path where they cannot do anything
The skill's docs are 92,691 bytes across 5 files. This run loaded
SKILL.md(19,036 B) andcontext/sync.md(32,996 B) in full: about 52 KB to execute five operations, all no-ops.context/sync.mdis 508 lines. Steps 4 and 5 are roughly 100 of them and include theinstall_newpolicy branches, the--setting-sourcescaveat, the reinstall-recurrence caveat, thenormalize-enabled-plugins.shcontract, thedefaultEnabledprecedence rule, and the project-scope enable-gap suppression ordering. Every one of those is unreachable whenmissing_from_user_installandmissing_from_enabledare both empty — which, on a fleet kept current, is the common case and was the case here.The gating signal is already in hand before either step: both arrays come from the
fleet-state.shreport that Step 1 precedes and that Steps 2 and 3 already read.Suggested fix: move Steps 4 and 5 into their own spoke, for example
context/sync-install-enable.md, and havesync.mdcarry a one-line pointer with its read condition — read this only whenmissing_from_user_installormissing_from_enabledis non-empty. That is the progressive-disclosure pattern the hub already uses forconverge.mdandscope-semantics.md, applied to the branch ofsync.mdthat is conditional in exactly the same way.This trims the common path without weakening the uncommon one, and it does not touch the two blocks that
SKILL.mddocuments as deliberate hub-only exceptions (the Report template and theinstall_newrender).Run evidence
Machine work totalled about 7 seconds; wall clock for the whole invocation was 189 seconds, and the difference is doc load and analysis rather than CLI time.
Environment
melodic-software,autoUpdate: oninstalled_plugins.json462,371 bytesVerification
Timing is reproducible with
date +%s.%Naround each call. Two incidental notes for anyone reproducing on this machine shape:bcis not present in this Git Bash install, so the arithmetic needsawk; and the guardrailsblock-windows-drive-tmpguard rejects/tmpas a scratch root, so scratch files need$TEMP.