Skip to content

fix(claude-ops): correct converge's settings-write claim and scope-keying gap - #2519

Merged
kyle-sexton merged 1 commit into
mainfrom
docs/claude-ops-converge-settings-write
Aug 12, 2026
Merged

fix(claude-ops): correct converge's settings-write claim and scope-keying gap#2519
kyle-sexton merged 1 commit into
mainfrom
docs/claude-ops-converge-settings-write

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

/claude-ops:plugins converge predicted that claude plugin uninstall -s project would leave a
project's committed .claude/settings.json untouched when that file carried no enabledPlugins
key. It does not. Running converge against 18 project-scope pins dirtied the tracked settings file
in both affected checkouts, and converge.md Step 5's wording ("can remove an enabledPlugins
entry") gives a run grounds to skip the diff check on exactly that wrong prediction.

Verified on Claude Code 2.1.228 with single calls against a clean tracked
.claude/settings.json, git-diffed after every step:

Call Writes committed settings? Effect
install -s project yes adds the id to enabledPlugins (true)
update -s project no existing doc claim re-verified, still holds
uninstall -s project yes removes the entry, leaves "enabledPlugins": {}

Both writing calls rewrite the whole file in Claude Code's key order, so unrelated sibling keys
move. The resulting blob was byte-identical to what the original converge run produced.

Changes:

  • converge.md Step 5 — check every touched project unconditionally (a clean tree after an
    uninstall is the surprising outcome, not a dirty one), and classify the diff before showing it:
    inert (empty map plus key reorder — recommend discarding, so a team-shared file carries no
    churn) or substantive (an entry actually removed — the user decides).
  • scope-semantics.md — new verified-facts section for install/uninstall, beside the update
    exemption, which is re-verified on the same version and left intact.
  • converge.md Step 2 — project scope keys on the working directory, not the repository.
    Verified by uninstalling one id in a repo's main checkout and watching its git worktree's record
    for the same id survive. Two checkouts sharing one .git and one tracked .claude/settings.json
    pin independently, so each needs its own cd.
  • sync.md — one observation on installed_plugins.json write timing (21/21 CLI-reported
    updates already visible to a post-sweep re-read). Logged explicitly as a single data point that
    does not retire the <new> fallback, since it shows only that the write landed before the
    re-read on that run.

Scoped to what was observed: no claim that the behavior holds on other Claude Code versions.

Test plan

  • scripts/affected-tests.sh --explain — every changed file is a recorded no-suite class; no shell
    suite selected.
  • markdownlint-cli2 on all four changed markdown files — 0 issues.
  • lychee --offline on the changed markdown — 0 errors (covers the new converge.md relative link).
  • check-skill.sh on plugins/claude-ops/skills/pluginsPASS, 0 errors, 0 warnings; runs the
    skill's own fleet-state.test.sh, which passed.
  • biome check and editorconfig-checker on the changed files — clean.
  • Reproduction teardown verified: the scratch project-scope install was removed, both scratch
    checkouts left clean, and fleet-state.sh reports 0 actionable divergences.

Related

Closes #2520

Found while running /claude-ops:plugins sync then converge on a real fleet — the converge run
produced an unexpected settings diff in two checkouts, which is what surfaced the wrong prediction.

Upstream anthropics/claude-code#81058 reports a related but more destructive variant on 2.1.220
(the enabledPlugins key deleted from user settings, disabling a surviving user-scope install).
That did not reproduce on 2.1.228 — the user-scope entries survived the converge run intact.

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 4s —— View job


Security Review

  • Gather context (PR diff, changed files)
  • Evaluate skip gate (closed PR / no security surface / already reviewed)
  • Report result

Skipped — no security-relevant surface. All five changed files are documentation and metadata:

  • plugins/claude-ops/.claude-plugin/plugin.json — version bump only (0.31.50.31.6)
  • plugins/claude-ops/CHANGELOG.md — changelog entry
  • plugins/claude-ops/skills/plugins/context/{converge,scope-semantics,sync}.md — prose corrections to how the plugins skill documents claude plugin install/update/uninstall -s project write behavior, plus wording clarifying that project scope keys on the working directory rather than the repo

No code, scripts, GitHub Actions workflows, permissions, credential handling, or executable logic is touched — the change corrects factual claims in skill guidance text about existing Claude Code CLI behavior. Nothing here introduces an injection surface, authorization gap, or supply-chain/trust-boundary risk, so per the security-review skill's skip gate this lane has nothing to report.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

Last security-reviewed head: 0d8cec45a8a22f35b486f7c9fd2df9994a966de1. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e6b212e51

ℹ️ 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".

Comment thread plugins/claude-ops/skills/plugins/context/converge.md
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 4m 40s —— View job


Claude Code Review

  • Gather context (PR diff, changed files, CLAUDE.md/REVIEW.md)
  • Review changed files for correctness/maintainability
  • Post findings (inline + summary)

Summary

Documentation-only fix (plugins/claude-ops): corrects converge.md's wrong prediction that
uninstall -s project might leave committed settings untouched, adds a verified-facts section to
scope-semantics.md, records the working-directory (not repository) scope-keying behavior for
git worktree checkouts, and logs one sync.md write-timing data point. The changes are careful
about hedging scope (version-gated, single-data-point caveats) and internally consistent with the
rest of converge.md/scope-semantics.md. CHANGELOG.md's new [0.31.6] entry and the
plugin.json version bump match the repo's existing convention.

Per this repo's REVIEW.md, this lane owns correctness/design/docs, not security — nothing here
is security-relevant regardless.

One finding posted inline on scope-semantics.md: the rewritten line ("converge is the only
action this skill runs that surfaces a settings diff",
scope-semantics.md#L56-L57)
is a broadened restatement of the pre-existing claim also carried in SKILL.md
(L36,
L52).
This PR's own new evidence a few lines below establishes that any call which sets an
enabledPlugins[id] entry rewrites the committed file — and sync's own Step 5 issues
claude plugin enable <id> -s project|local
(sync.md#L175)
unconditionally for currentProject: true entries — the same map mutation install was just
verified to make. Nothing here confirms enable is exempt, so sync (default, weekly-cadence,
non-destructive-tier) may silently dirty a team-shared committed file with no diff ever surfaced —
the same failure category this PR fixes for converge. Worth either hedging the claim to "the only
action verified to surface a settings diff" or giving enable the same empirical check
install/uninstall/update just got.

Everything else in the diff (the converge.md Step 5 rewrite, the worktree-keying note, the
sync.md write-timing note) held up under review — no other issues found. Note: the PR's Test plan
(markdownlint, lychee, check-skill.sh, biome, reproduction teardown) is author-claimed and was
not independently re-run in this review.

Branch

Comment thread plugins/claude-ops/skills/plugins/context/scope-semantics.md
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

…ying gap

converge.md Step 5 said `uninstall -s project` "can remove an enabledPlugins
entry" from committed settings, which reads as "a clean tree means nothing was
written" — so a run that predicted no diff had grounds to skip the check. It
always writes. Verified on Claude Code 2.1.228 with single calls against a clean
tracked .claude/settings.json: it empties the map to `"enabledPlugins": {}`
rather than deleting the key, writes the key even into a file that never had
one, and rewrites the file in Claude Code's key order so unrelated sibling keys
move. Step 5 now checks every touched project unconditionally and classifies the
diff as inert or substantive, because the two warrant opposite advice.

scope-semantics.md gains that as a section beside the update exemption, which
was re-verified on the same version under the hardest available conditions and
still holds.

Also recorded: project scope keys on the working directory, not the repository —
two checkouts of one repo pin independently, so converge must keep them as
separate rows with separate cd targets. And one observation on
installed_plugins.json write timing, logged as a data point that does not retire
sync.md's <new> fallback.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kyle-sexton
kyle-sexton force-pushed the docs/claude-ops-converge-settings-write branch from 4e6b212 to 0d8cec4 Compare August 12, 2026 21:57
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 20s —— View job


Security Review

  • Gather context (PR diff, changed files, prior review state)
  • Evaluate skip gate (closed PR / no security surface / already reviewed)
  • Report result

Skipped — no security-relevant surface, and unchanged since the last review.

The head SHA moved from 4e6b212e51a52ac6c690b9adf183500e4aa13cd1 to 0d8cec45a8a22f35b486f7c9fd2df9994a966de1, but this is a rebase artifact, not new content: git diff origin/main...HEAD for this PR is byte-for-byte the same 5 files as before. The rebase pulled in an unrelated commit already merged to main (fa762fb, the hook-utils.sh temp-file fix for #1595), which forced the plugin version bump from 0.31.60.31.7 to avoid a CHANGELOG.md collision. None of that is this PR's own diff.

This PR's actual changes remain:

  • plugins/claude-ops/.claude-plugin/plugin.json — version bump only
  • plugins/claude-ops/CHANGELOG.md — changelog entry
  • plugins/claude-ops/skills/plugins/context/{converge,scope-semantics,sync}.md — prose corrections to how the plugins skill documents claude plugin install/update/uninstall -s project write behavior, plus a clarification that project scope keys on the working directory rather than the repository

No code, scripts, GitHub Actions workflows, permissions, or executable logic is touched. The three context/*.md files are skill guidance text read by Claude, not command templates or scripts — nothing in the diff introduces an injection surface, authorization gap, or supply-chain/trust-boundary risk. Per the security-review skill's skip gate, this lane has nothing to report.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 1s —— View job


Claude Code Review

  • Gather context (PR diff at HEAD 0d8cec4, changed files, CLAUDE.md/REVIEW.md)
  • Review changed files for correctness/maintainability per /review:code-review
  • Post findings (inline + summary)

Summary

Documentation-only fix in plugins/claude-ops (converge.md, scope-semantics.md, sync.md,
CHANGELOG.md, plugin.json). Verified the version bump (0.31.60.31.7) matches the new
CHANGELOG entry and origin/main's prior state. converge.md Step 5's rewrite (checking every
touched project unconditionally, classifying the diff as inert vs. substantive) is internally
consistent with the new evidence in scope-semantics.md, and correctly scoped to -s project
(committed) rather than -s local (gitignored, out of scope for this fix).

Two findings posted inline, both re-flagging correctness gaps this same PR's own new evidence
exposes — both were already raised by other reviewers on this PR and remain unresolved in the
current push:

  1. converge.md:63
    "Project scope keys on the working directory, not the repository" overgeneralizes from the
    worktree experiment. Independently verified against
    fleet-state.sh:207-214
    (resolves via CLAUDE_PROJECT_DIR or git rev-parse --show-toplevel, i.e. checkout root — not
    literal cwd) and
    fleet-state.test.sh:335-370
    (a nested subdirectory of a checkout resolves currentProject: true against the same
    checkout-root record, not an independent one). The worktree finding itself is real; the wording
    just needs to say "checkout root" rather than "working directory."
  2. scope-semantics.md:57
    "converge is the only action this skill runs that surfaces a settings diff" is contradicted by
    this PR's own new section two lines below: any call that sets enabledPlugins[id] rewrites the
    committed file. sync.md's own Step 5
    (sync.md:175)
    issues claude plugin enable <id> -s project|local unconditionally for gap-filling entries —
    the same map mutation just verified for install. Nothing establishes enable is exempt, so
    sync (default, non-destructive-tier) may silently dirty a team-shared committed file the same
    way this PR fixes for converge.

Everything else — the Step 5 inert/substantive classification logic, the sync.md write-timing
note (correctly hedged as one data point), the version bump/CHANGELOG consistency — held up under
review. Per this repo's REVIEW.md, security is scoped to the dedicated security lane; nothing in
this documentation-only diff is security-relevant regardless.

Note: the PR's Test plan (markdownlint-cli2, lychee --offline, check-skill.sh, biome check,
reproduction teardown) is author-claimed and was not independently re-run in this review.

Branch

Comment thread plugins/claude-ops/skills/plugins/context/converge.md
Comment thread plugins/claude-ops/skills/plugins/context/scope-semantics.md
@kyle-sexton
kyle-sexton merged commit d31cef4 into main Aug 12, 2026
38 checks passed
@kyle-sexton
kyle-sexton deleted the docs/claude-ops-converge-settings-write branch August 12, 2026 22:21
kyle-sexton added a commit that referenced this pull request Aug 12, 2026
…ns call (#2527)

## Summary

Follow-up to #2519, which merged before its review findings were
addressed. Both findings were real;
both are now confirmed by experiment on Claude Code 2.1.228 rather than
by wording judgment.

**1. "`converge` is the only action this skill runs that surfaces a
settings diff" was false.**
`sync.md` Step 5 issues `claude plugin enable <id> -s <that scope>`, and
`enable` sets the same
`enabledPlugins[id]` key `install` was verified to write. Tested, one
call each against a clean
tracked `.claude/settings.json`:

| Call | Writes `.claude/settings.json`? |
|---|---|
| `install <id> -s project` | yes |
| `uninstall <id> -s project` | yes |
| `enable <id> -s project` | yes (adds the id, `true`) |
| `disable <id> -s project` | yes (adds the id, `false`) |
| `update <id> -s project` | no |

`-s local` writes the gitignored `.claude/settings.local.json` and never
touches the tracked file —
tested, not assumed, since Step 5 issues both. So `sync`, the default
action, can dirty a
team-shared tracked file with no diff surfaced. This change documents
that exposure and tells Step 5
to name a `-s project` enable in the report; **giving Step 5 converge's
full diff-surfacing is a
behavior change to the default action and is deliberately left to its
own PR.**

Also recorded: `enable -s project` gates on the *merged effective* value
— enabling an id that is
`true` only at user scope fails rather than writing a project entry.

**2. Project-scope keying: both the original wording and the proposed
correction were wrong.**
#2519 said "keys on the working directory"; review proposed "checkout
root", citing `fleet-state.sh`
and its test. Testing settled it against both:

- The **CLI** keys on the literal cwd — `install -s project` from
`<checkout>/nested/subdir` recorded
that subdirectory as `projectPath` and created its own
`nested/subdir/.claude/settings.json`.
- **`fleet-state.sh`** resolves the checkout root
(`fleet-state.sh:211-221`, read directly rather
than taken from the review), and `fleet-state.test.sh` pins the
nested-subdirectory case.

The layers disagree, and the gap is the finding, not either wording: a
project-scope install from
below the checkout root never matches `currentProject`, never updates,
and never appears in a
divergence row — while still loading in that subtree. Recorded in
`gotchas.md`, where the sibling
silent-mismatch gotchas live. `converge.md` Step 2 keeps only the
worktree consequence the evidence
supports.

## Test plan

- `scripts/affected-tests.sh --run` — 3 suites selected, all pass
(`check-skill.test.sh`,
  `check-changed-skills.test.sh`).
- `markdownlint-cli2` on all changed markdown — 0 issues.
- `lychee --offline` — 0 errors.
- `check-skill.sh` on `plugins/claude-ops/skills/plugins` — **PASS**, 0
errors, 0 warnings.
- `plugin.json` parses; version 0.31.7 → 0.31.8.
- The released `## [0.31.7]` CHANGELOG section was diffed against
`origin/main` and is byte-identical
— this PR adds `## [0.31.8]` above it rather than editing shipped
release notes.
- Every experiment torn down: scratch project-scope installs removed,
the `nested/` tree deleted, the
  scratch `settings.local.json` removed, and both checkouts left clean.

## Related

Closes #2526

Follows #2519. Upstream context for the underlying CLI behavior is in
`anthropics/claude-code#81058`, where this session's 2.1.228 findings
are posted as a comment.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

claude-ops plugins skill: converge predicts the wrong settings-write behavior for uninstall -s project

1 participant