fix(claude-ops): remediate the plugins:sync audit findings (0.36.0) - #3148
Conversation
|
Claude finished @kyle-sexton's task in 3m 2s —— View job Security review in progress
|
|
Last security-reviewed head: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fc18ad77a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @kyle-sexton's task in 5m 7s —— View job Code review complete
This repo carries a Scope reviewed: all 11 changed files — Findings: 1 nit, posted inline.
What held up well under review (no issues found):
Not independently verified (author-claimed only, noted per this lane's rules): the live-machine numbers in the PR description/CHANGELOG (66→0 update calls, the 9-marketplace resolution breakdown, the 2.1.240 CLI probes) and the test-run/lint-tool pass counts in the Test plan — I did not have a sandboxed way to execute |
|
Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
5fc18ad to
94d8869
Compare
|
Claude finished @kyle-sexton's task in 3m 12s —— View job Security review complete
Per this lane's scope ( Findings: none. Reviewed the security-relevant surface introduced/changed in this PR at
Consistent with the prior code-review pass on this head and the security scan at the prior head |
Closes the post-use audit of `claude-ops:plugins`' `sync` action. Every harness-behaviour claim added or changed here was verified on Claude Code 2.1.240. Three silent-failure paths closed: - Step 2 no-opped invisibly. `fleet-state.sh` computes `currentProject` as a correct tri-state, but `null` covers "user-scope record", "no projectPath", and "no project context resolved at all" alike, so a run from $HOME and a run inside a repo with no in-repo installs produced an identical report. A new top-level `project_root` field carries the distinction and the report gains a fixed `In-repo:` row that must state a skip. - `sync all` refreshed every marketplace, then ran install/update/enable maintenance against exactly one. Steps 2-5 are now the per-marketplace loop body, every `--ids` call carries `--marketplace`, and a new `--marketplaces` mode enumerates the names CR-free so the loop needs no hand-written jq. - Actionable divergences routed to a `converge` command that cannot execute, because `-s project` has no path flag and the recorded `projectPath` no longer exists. Those rows now get their own report section outside the Divergences count, and `converge` emits them as blocked rather than as runnable commands. Adds a catalog-version pre-filter for the Step 3 sweep. Each plugin's version lives in the marketplace checkout even though the catalog entry carries none, so `--ids update-candidates-user` can withhold ids it proved are already current -- 0 update calls instead of 66 on an already-current fleet. It fails open by construction: an unreadable catalog version yields a candidate, and the test suite proves the degradation as a byte-equality against `installed-user`. The manifest it reads must sit inside the marketplace checkout, enforced physically rather than lexically: `source` is third-party content, withholding an update is the only unsafe direction, and a symlink inside the checkout is reached by an ordinary `./name` source no string check can see. Also corrects the `pluginConfigs` scope claim (user/--settings/managed only; project and local ignored since v2.1.207, while `enabledPlugins` still honors them), restates the `/reload-plugins --force` trigger as prompt-cache invalidation with the docs' two-step recovery, splits the divergence count into run-caused vs pre-existing, and reduces the `versionsMatch` rule to one origin. `setup`: the headless `--config` route no longer prescribes an uninstall/reinstall cycle, and separates the write from the running session's behavior (a same-session `check` still reports the old value). Wording matches the fleet-wide correction landed in #3115 verbatim apart from this plugin's own option list, closing a live contradiction on main where the regenerated README carried the corrected guidance while this skill still prescribed the destructive cycle. fleet-state.test.sh: 45 -> 73 cases, covering each behaviour change. Two mechanisms adopted from the concurrent duplicate implementation in #3145, which an autonomous work-items lane opened against the same issue: the `--marketplaces` enumeration mode, which closes a real gap here (this branch told the model to loop per marketplace without giving it a CR-safe way to list them), and the snake_case `project_root` spelling, which matches the existing top-level key convention where this branch had used camelCase. Its `stale-user` selector name and `plugins/<name>` layout fallback were deliberately not taken: the former reads as an authoritative stale list rather than a candidate superset, and the latter guesses a layout the catalog did not declare, which can withhold on an unverified assumption. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94d8869 to
17a7ddc
Compare
|
Claude finished @kyle-sexton's task in 3m 2s —— View job Security review complete
Per this lane's scope ( Findings: none. Reviewed the full PR diff (
This is consistent with the prior security passes on earlier heads of this PR, which reached the same conclusion on this same logic; the code in the security-relevant sections ( |
|
Claude finished @kyle-sexton's task in 37s —— View job Code review skipped
|
Closes #3182 ## Summary Post-merge verification of #3139 (merged as `ef4d53959`) found three defects in the prose it shipped, each the same class that PR existed to correct: **a claim or rule reaching past what backs it.** They were filed rather than quietly patched because the content was already on `main`. This fixes those three plus the smaller items #3182 lists, in 46 added lines across three files. ## Fix **1. The fetch prohibition's stated cause did not entail its stated rule.** `PLUGIN-PHILOSOPHY.md` sanctioned "a documentation URL", then condemned any skill instructed to fetch it as having "made the publisher a runtime dependency". Fetching `code.claude.com` creates no such dependency, and a practice already shipping in the tree was condemned by it. The prohibition now turns on the target's owner and reaches publisher-owned targets only. For those targets, distinguishing an instruction to fetch from a citation offered for a reader is genuinely hard, and the statement says so rather than implying it has been settled — `plugins/architecture/reference/topic-docs.md` is named as the open case, and no ticket owns it (#3136 is enforcement-site consolidation, not this). **2. The `evidence-bearing` bullet was unsatisfiable as worded.** It required setup to report "the effective value it observed", while the same section pins *effective value* to running-session behaviour and directs verification to a fresh session. A same-session run can only observe the **stored** value. One word: `effective` → `stored`. **3. A narrowing presented as a faithful clarification.** The hook-plugin eval skip stated its rationale as "no model-facing skill at all" where the prior text said "no model-**invoked** skill". Neither works: a `setup` skill sets `disable-model-invocation: true`, so either phrasing is satisfied by a plugin that ships one — admitting as skips exactly the plugins the rest of the rule excludes. The defect was stating the condition in terms of invocation mode at all. It now reads **"no skill carrying a judgment-bearing contract"**, the test the warrant rule two sentences above already uses. Outcome unchanged: 19 hook plugins ship a setup skill, all 19 carry setup evals. **Smaller items.** Both paired reconfiguration sites in `MIGRATION-PLAYBOOK.md` now name the readback location and agree in substance, including the sensitive-value limit — an asymmetry between them would have sent a reader reconfiguring a sensitive option at project scope to look in user settings, find nothing, and report a failed write, which is the false failure `PLUGIN-PHILOSOPHY.md` exists to prevent. Their provenance cites seam 1, which documents both halves, rather than smoke-test C, which explicitly disclaims covering a sensitive option. The "step 3 above" cross-reference — which pointed from inside Reintegration's step 1 at Reintegration's own step 3, about verify-before-retiring — is replaced by a direct citation of seam 1. The `github.test.sh` sweep's wider/narrower axes are named, and "both steps of the same job" is corrected to "each running in its own step". The workflow header's gate description is corrected: the pinned reusable requires four sections, not a closing keyword plus `## Related`. ## Verification Local gates at the final commit: `markdownlint-cli2` 0 issues; `check-contract-clause-coverage.py` exit 0; `lychee --offline` 0 errors across 103 unique links; `zizmor` no findings. The workflow change is comment-only, confirmed by diff. Six fresh-context verification passes, each given the bounded criteria plus an unbounded criterion instructing it to hunt for claims reaching past their evidence anywhere in the touched paragraphs. **All six returned FAIL**, and each round's fixes introduced at least one new instance of the defect being repaired. The sixth pass found two, both in the single paragraph this PR had to *write* rather than cut, and both repairs were deletions: a tracker-wide "no ticket owns that question" that the tracker contradicts (#432 carries an accepted ruling on it, and `scripts/skill-portability-tokens.txt` stages a lint class blocked on that ruling), and a hedge that denied the statement its own preceding clause had just made. Every deletion from the prior round verified clean against the tree. Reviewing where the findings came from settled the approach. Items 2 and 3 were clean from round three onward; essentially every finding from round two on landed in material added *beyond* what #3182 asked for — an enumeration of nonconforming instances, a paragraph grounding the prohibition against the tree, a rewritten security rationale, a sensitive-value carve-out. Each was written to close the previous round's finding and opened one or two of its own. The final revision deletes those elaborations rather than repairing them again, which is why the diff is 46 lines rather than the 2,926 it peaked at. Twenty-five instances of the defect class were found across the five rounds. One was caught by the author re-reading their own writing, four by the review bots, and the rest by fresh-context verification. **None by self-review.** Two things generalise. A verifier is bounded by its criteria, so a defect nobody names survives any number of green passes — every round's findings came from the unbounded criterion, not the checklist. And under-claiming is not the safe direction: round four's findings were mostly repairs to what round three's *removals* broke. Both directions are the same failure to say exactly what the evidence supports. One pre-existing defect is deliberately left alone and filed as #3184: the workflow header's security rationale ("reads PR body metadata from the event payload only") is false against the pinned reusable, which live-refetches. It is outside #3182's scope, the `zizmor` suppression it backs is independently sound, and three separate rewrites of that comment block each introduced a new inaccuracy. ## Related - #3182 — the issue this closes; its line references were verified against `origin/main` at `ef4d53959`. - #3139 — introduced this prose; its own post-merge verification found these defects and filed them rather than patching silently. - #3184 — the workflow-comment defects this PR deliberately did not rewrite. - #3173 — shipped part 1 into the same files; item 3's prior wording is its text. - #3136 — enforcement-site consolidation; it does not own the cite-versus-fetch question, and this PR no longer claims it does. - #3115 / #3116 / #3148 — the rest of the campaign whose doc half #3139 was. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

Closes #3112
Summary
Remediates the post-use plugin-quality audit of
claude-ops:plugins'syncaction — all 15 findings, plus a catalog-version pre-filter worth more than the rest combined. Every harness-behaviour claim added or changed here is version-stamped against Claude Code 2.1.240, the CLI it was verified on.Three silent-failure paths closed, all of the same shape: correct-looking output from a path that did not run.
fleet-state.shcomputedcurrentProjectas a correct tri-state, butnullcovers "user-scope record", "noprojectPath", and "no project context resolved at all", so a run from$HOMEand a run inside a repo with no in-repo installs produced an identical report. New top-levelproject_rootcarries the distinction; the report gains a fixedIn-repo:row that must state a skip.sync allrefreshed every marketplace, then ran install/update/enable maintenance against exactly one. Steps 2–5 are now the per-marketplace loop body, every--idscall carries--marketplace, and a new--marketplacesmode enumerates the names CR-free so the loop needs no hand-writtenjq.convergecommand that cannot execute, because-s projecthas no path flag and the recordedprojectPathno longer exists. Those rows now get their own report section, outside the Divergences count, andconvergeemits them as blocked rather than runnable.Design decision on F2, which the brief left to me: stale project records are classified and reported, never converged and never reaped. No
claude pluginverb removes an install record by path, editinginstalled_plugins.jsonis outside this skill's boundary, and claude-ops must not read another plugin'suserConfigto decide whose directories these were — so the wording is deliberately generic about "a tool that owns those directories' lifecycle" rather than naming one.projectPathPresentis advisory and may never filter: an unmounted volume, an offline share, and unplugged removable media are indistinguishable from a deleted worktree to a directory test, so the report says "not present on this machine", never "dead".Fix
Catalog-version pre-filter (the performance finding).
marketplace.jsonentries carry no version — which is why Step 3 calledclaude plugin updatefor every user-scope install and let the CLI no-op. Each plugin's version does exist in the marketplace checkout at<installLocation>/<entry.source>/.claude-plugin/plugin.json.fleet-state.shnow exposes it ascatalog_versions, and--ids update-candidates-userwithholds only ids it positively proved already sit at the catalog version. On this machine's already-current fleet: 66 update calls → 0.It fails open by construction, and that is the dominant path, not an edge case. Measured across nine registered marketplaces: resolves fully for five, partially for two (13/53, 1/15), not at all for two. Any unreadable version yields a candidate, exactly as if no pre-filter existed. The test asserts the degradation as a byte-equality against
installed-user, not a spot check. The pre-filter is also disabled — falling back to the unfilteredinstalled-user— for any marketplace whose Step 1 refresh failed, since a stale checkout cannot prove an id current; andauditmode, which never refreshes, reports its prediction as a lower bound carrying the catalog'slastUpdated.The manifest must sit inside the checkout, enforced physically.
sourceis third-party content and the only unsafe direction is withholding an update. A lexical../refusal is insufficient — a symlink inside the checkout is reached by an ordinary./namesource no string check can see — so the resolved manifest path is required to sit under the resolved checkout root with symlinks followed.Also fixed:
pluginConfigsread-path claim (user /--settings/ managed only; project and local ignored since v2.1.207, whileenabledPluginsstill honors them);/reload-plugins --forcerestated as the docs' two-step with prompt-cache invalidation as the trigger; divergence count split into run-caused vs pre-existing; self-update row when the sweep updatesclaude-opsitself; TOCTOU wording matched to the implementation with the inert detector replaced;versionsMatchreduced to one origin plus pointers;user_scope_orphansfor the structurally-invisible single-scope orphans.setup— now load-bearing, not tidiness. #3115 regenerated this plugin's README with the corrected reconfiguration guidance whileskills/setup/SKILL.mdstill prescribed the destructive uninstall/reinstall cycle — a contradiction live onmainthat #3115's own reviewer flagged and could not fix, because that file is in this change set's fence. This PR closes it, matching the landed fleet-wide wording verbatim apart from this plugin's option list, including the distinction that the write lands while the running session's behavior does not (${user_config.*}is injected at skill load andCLAUDE_PLUGIN_OPTION_*comes from an environment fixed at session start, so a same-sessioncheckstill reports the old value and reading that as a failed write is wrong). Also corrects an inherited miscount: 15 options and 8*_audit_enabledtoggles, not 14 and 7.Deferred, with reasons in the CHANGELOG:
--run-loginfleet-state.sh(conflicts with the script's read-only contract; F12's cheapest tier landed instead), a fourthinstall_newvalue (needs durable state), F8's--selfcheck, F14 (the repo's owncheck-changed-skills.shenforces trigger-keyword preservation, and the finding is cosmetic since the skill setsdisable-model-invocation: true), and the upstream issue for the missing record-reaping verb.Verification
Two independent fresh-context verifier agents reviewed the final state against the sealed audit packet. Both initially returned FAIL; both sets of defects were fixed and re-verified.
Round 1 found the change had made Step 3 catalog-dependent without extending the stale-catalog deferral rule — a stale checkout could withhold an id as "already current". Fixed at the decision point, plus three unstamped claims and an over-flattering CHANGELOG figure.
Round 2 found the symlink-containment hole above, and that
auditmode never triggers the stale-catalog fallback. Both fixed.Worth recording: the symlink regression test initially failed for the wrong reason — Git Bash's
ln -ssilently deep-copies instead of linking, so the fixture was asserting against a real in-checkout directory and9.9.9was the correct answer for what was on disk. The test now creates a genuine symlink viaMSYS=winsymlinks:nativestrict, gates on[[ -L ]], and skips honestly where the platform yields no real symlink. Verified against real symlinks from bothln -sandcmd mklink /D.Test plan
fleet-state.test.sh: 45 → 73 cases, 0 failed, covering every behaviour change —catalog_versionsand all four fail-open inputs, the fail-open byte-equality proof,update-candidates-user(equal/behind/ahead/project-scope/partial), symlink containment plus an in-checkout control, the productioninstallLocationbranch (which bypasses the fixture override),projectPathPresentincluding thefalse-is-not-nullguard and the never-filters guarantee,user_scope_orphans,project_root(including that every--allblock carries it),--marketplaces(enumeration, empty, standalone-flag rejection order-independence, and a CR regression under a CRLF-emittingjqstub), and selector help-text drift across all six selectors.check-changed-skills.sh— 2 skills, 0 errors.validate-plugin-contracts.mjs— 2813 files.check-shell-portability.sh,check-skill-portability.sh,check-evals-quality.sh,check-changelog-parity.sh(--check,--check-order,--check-bump,--check-preserved),shellcheck,typos,markdownlint-cli2,check-fixture-git-isolation.sh,claude plugin validate, catalog/cheatsheet/plugin-options sync — all pass.user_scope_orphansreturned exactly the four ids the corrected packet names, and all 45 project-scope records came backprojectPathPresent: false.Related
skills/setup/SKILL.md) and covers all 15 audit findings rather than the issue's summary of 8. Two of its mechanisms were adopted with attribution in the commit message: the--marketplacesenumeration mode, which closes a real gap here, and the snake_caseproject_rootspelling, which matches the existing top-level key convention where this branch had used camelCase. Two were deliberately not taken — itsstale-userselector name reads as an authoritative stale list rather than a candidate superset, and itsplugins/<name>layout fallback guesses a path the catalog did not declare, which can withhold an update on an unverified assumption. fix(claude-ops): remediate the plugins:sync audit findings (0.36.0) #3145 also lacks the traversal and symlink containment checks.--configclaim prescribes a destructive reinstall across 18 setup skills #3111 / fix(plugins): correct the unstamped --config reconfiguration claim (#3111) #3115 — workstream A, merged as1f7525fe6. This branch is rebased on it; the predicted two-file conflict was resolved by keeping0.36.0and stacking both CHANGELOG entries with## [0.35.4]unmodified (--check-preservedconfirms all 107 headings survive).af3fea480. C makessource-controldrop project-scope records at worktree teardown, the producer-side counterpart to this PR's classify-and-report handling. This PR deliberately does not name or depend onsource-control.plugin-qualityevidence packet;evidence-3.mdre-scopes F5 andevidence-2.mdcorrects a tally, both applied here.🤖 Generated with Claude Code