fix(claude-ops): remediate the plugins:sync audit findings (0.36.0) - #3145
fix(claude-ops): remediate the plugins:sync audit findings (0.36.0)#3145kyle-sexton wants to merge 4 commits into
Conversation
Extend the plugins skill's read-only fleet inspection script so the sync doc surfaces can consume real state instead of collapsed signals: - project_root (string|null) on every emitted marketplace block, incl. each per-marketplace block under --all: disambiguates "no project context at all" from "project context with zero in-repo installs", which --ids current-project alone projects identically to zero ids. - projectPathExists (true/false/null) on installed[] records and divergences[].scopes[] entries: converge/sync must not emit (cd "<projectPath>" && claude plugin ...) against directories that no longer exist; no CLI verb reaps such records on the audited CC 2.1.240 run, so they are report-only. - --marketplaces: standalone CR-free per-line enumeration of known_marketplaces.json names so sync's all mode can iterate --marketplace <name> --ids <selector> without hand-writing the jq -r | while read that corrupts ids under native-Windows jq. Combining it with --marketplace/--all/--ids is a usage error (exit 2). - --ids stale-user: user-scope installed ids NOT confirmed current with the local marketplace checkout's catalog version (manifest at <installLocation>/<source-path>/.claude-plugin/plugin.json), failing open on every unresolvable edge so a wrong emission costs one no-op update while a wrong omission can never skip a real one. On the audited CC 2.1.240 run this comparison predicted exactly the 48 of 61 plugins that updated with 0 false positives/negatives, all catalog manifests resolving locally with zero network. Test suite grows from 45 to 56 cases covering the projectPathExists tri-state, project_root string/null/--all shapes, --marketplaces enumeration + CRLF regression + combination rejections, and every stale-user fail-open edge; no pre-existing case was modified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RJjit7ErqbTpZ94KbVMUs
…dit fixes (0.36.0) Phase 2 of the plugins-skill sync audit remediation: the doc surfaces now consume the four fleet-state.sh capabilities Phase 1 shipped. sync.md Step 2 branches on project_root for the zero-record tri-state and SKILL.md's report gains the always-present In-repo row; every (cd "<projectPath>" && ...) site gates on projectPathExists with orphaned install records as a named report-only category; sync all iterates Steps 2-5 per marketplace via --marketplaces; a conditional self-update Note row states the run executed the pre-update algorithm; the pluginConfigs read-scope claim is corrected (user/--settings/managed only since CC v2.1.207, contrasted with enabledPlugins); version capture mandates a mktemp ledger with per-call appends; Step 3 consumes --ids stale-user (fail-open, 48/61 validated on the audited CC 2.1.240 run); and the versionsMatch filter rule is normative only in scope-semantics.md with every other site pointing at it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RJjit7ErqbTpZ94KbVMUs
|
Claude finished @kyle-sexton's task in 1m 26s —— View job Claude finished the security review
|
|
Last security-reviewed head: |
|
Claude finished @kyle-sexton's task in 7m 26s —— View job CI Code ReviewTodo
This is an unusually well-engineered doc/script pair — extensive inline rationale, deliberate fail-open semantics, and a real fixture-based test suite. I read the full diff plus the whole current
Both are posted as inline comments with more detail. Nothing else rose to the "a careful senior reviewer would block" bar — the Not independently verified: I could not execute Branch: |
|
Claude has reviewed this PR 3 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
…ctor Two review findings on the plugins skill: - fleet-state.sh selector table: `installed-user` still claimed `(Step 3 update)` after sync.md Step 3 was retargeted exclusively to `stale-user`. Retag it as the full user-scope set whose not-confirmed-current subset (stale-user) Step 3 consumes. Comment-only. - SKILL.md used "unknown/orphaned plugin id" for the catalog-mismatch concept, colliding with the new "orphaned install record" (dead projectPath) category. Drop "orphaned" from the old phrase and define it inline; "orphaned" now appears only in the dead-path sense. Update the three matching catalog-mismatch phrasings in evals.json to keep the graded expectations consistent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RJjit7ErqbTpZ94KbVMUs
|
Claude finished @kyle-sexton's task in 32s —— View job Security review — no issues found
Re-reviewed following the
Neither touches No CRITICAL, IMPORTANT, or SUGGESTION-level security findings to report. |
|
Claude finished @kyle-sexton's task in 2m 23s —— View job CI Code ReviewTodo
This lane already reviewed this PR twice at Both prior findings are correctly resolved:
No new issues found in the rest of the diff. Specifically checked and found sound:
Not independently verified: as in the prior round, this environment's tool policy blocks running No CRITICAL/IMPORTANT findings to report on this pass. Branch: |
…hangelog interleave) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RJjit7ErqbTpZ94KbVMUs
|
Claude finished @kyle-sexton's task in 1m 6s —— View job CI Code ReviewTodo
Skip gate hit: this head already has a successful review from this lane that still applies. This lane reviewed the PR twice already — at
Both are pre-existing Per this lane's skip gate, no further review is warranted — nothing new to post. |
|
Claude finished @kyle-sexton's task in 56s —— View job Security review — no issues found
Re-reviewed following the This carries forward the conclusion from the prior passes: No CRITICAL, IMPORTANT, or SUGGESTION-level security findings to report. |
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>
|
No description provided. |
|
Closing as a duplicate of #3148 (same title/scope: "remediate the plugins:sync audit findings (0.36.0)"). Evidence:
Recommend continuing work on #3148 (currently has unresolved bot-review threads + a behind-main blocker being worked) and abandoning this one. Reopen if #3148 turns out to be missing something #3145 covered — diff the two file lists above. |
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>
|
Closing as superseded by #3148, after a file-by-file comparison rather than a size judgement. This PR is a real implementation, not a duplicate to be discarded lightly. It covers the same core findings, bumps to the same
Why #3148 lands instead:
Two of this PR's choices were deliberately not taken, for the record: the On why this happened at all — filing #3112 as a tracking issue to satisfy the Thanks to whatever ran this — the |
…3148) Closes #3112 ## Summary Remediates the post-use plugin-quality audit of `claude-ops:plugins`' `sync` action — 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. - **F1 (high)** — Step 2, the self-described "primary value path", no-opped invisibly. `fleet-state.sh` computed `currentProject` as a correct tri-state, but `null` covers "user-scope record", "no `projectPath`", *and* "no project context resolved at all", so a run from `$HOME` and a run inside a repo with no in-repo installs produced an identical report. New top-level `project_root` carries the distinction; the report gains a fixed `In-repo:` row that must state a skip. - **F4 (medium-high)** — `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`. - **F2 (high)** — every actionable divergence 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 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 plugin` verb removes an install record by path, editing `installed_plugins.json` is outside this skill's boundary, and claude-ops must not read another plugin's `userConfig` to decide whose directories these were — so the wording is deliberately generic about "a tool that owns those directories' lifecycle" rather than naming one. `projectPathPresent` is 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.json` entries carry no version — which is why Step 3 called `claude plugin update` for 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.sh` now exposes it as `catalog_versions`, and `--ids update-candidates-user` withholds 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 unfiltered `installed-user` — for any marketplace whose Step 1 refresh failed, since a stale checkout cannot prove an id current; and `audit` mode, which never refreshes, reports its prediction as a lower bound carrying the catalog's `lastUpdated`. **The manifest must sit inside the checkout, enforced physically.** `source` is 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 `./name` source no string check can see — so the resolved manifest path is required to sit under the resolved checkout root with symlinks followed. Also fixed: `pluginConfigs` read-path claim (user / `--settings` / managed only; project and local ignored since v2.1.207, while `enabledPlugins` still honors them); `/reload-plugins --force` restated 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 updates `claude-ops` itself; TOCTOU wording matched to the implementation with the inert detector replaced; `versionsMatch` reduced to one origin plus pointers; `user_scope_orphans` for the structurally-invisible single-scope orphans. **`setup` — now load-bearing, not tidiness.** #3115 regenerated this plugin's README with the corrected reconfiguration guidance while `skills/setup/SKILL.md` still prescribed the destructive uninstall/reinstall cycle — a contradiction live on `main` that #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 and `CLAUDE_PLUGIN_OPTION_*` comes from an environment fixed at session start, so a same-session `check` still reports the old value and reading that as a failed write is wrong). Also corrects an inherited miscount: 15 options and 8 `*_audit_enabled` toggles, not 14 and 7. **Deferred, with reasons in the CHANGELOG:** `--run-log` in `fleet-state.sh` (conflicts with the script's read-only contract; F12's cheapest tier landed instead), a fourth `install_new` value (needs durable state), F8's `--selfcheck`, F14 (the repo's own `check-changed-skills.sh` enforces trigger-keyword preservation, and the finding is cosmetic since the skill sets `disable-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 `audit` mode never triggers the stale-catalog fallback. Both fixed. Worth recording: the symlink regression test initially failed for the wrong reason — Git Bash's `ln -s` silently deep-**copies** instead of linking, so the fixture was asserting against a real in-checkout directory and `9.9.9` was the correct answer for what was on disk. The test now creates a genuine symlink via `MSYS=winsymlinks:nativestrict`, gates on `[[ -L ]]`, and skips honestly where the platform yields no real symlink. Verified against real symlinks from both `ln -s` and `cmd mklink /D`. ## Test plan - `fleet-state.test.sh`: **45 → 73 cases, 0 failed**, covering every behaviour change — `catalog_versions` and 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 production `installLocation` branch (which bypasses the fixture override), `projectPathPresent` including the `false`-is-not-`null` guard and the never-filters guarantee, `user_scope_orphans`, `project_root` (including that every `--all` block carries it), `--marketplaces` (enumeration, empty, standalone-flag rejection order-independence, and a CR regression under a CRLF-emitting `jq` stub), 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. - Live read-only smoke independently reproduced the audit's own numbers before any test was written: `user_scope_orphans` returned exactly the four ids the corrected packet names, and all 45 project-scope records came back `projectPathPresent: false`. ## Related - Closes #3112 (workstream B of the audit remediation). - **#3145 — a concurrent duplicate implementation of the same issue**, opened by an autonomous work-items lane ~35 minutes before this PR. This PR is a strict file-superset of it (its ten files plus `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 `--marketplaces` enumeration mode, which closes a real gap here, and the snake_case `project_root` spelling, which matches the existing top-level key convention where this branch had used camelCase. Two were deliberately not taken — its `stale-user` selector name reads as an authoritative stale list rather than a candidate superset, and its `plugins/<name>` layout fallback guesses a path the catalog did not declare, which can withhold an update on an unverified assumption. #3145 also lacks the traversal and symlink containment checks. - #3111 / #3115 — workstream A, **merged** as `1f7525fe6`. This branch is rebased on it; the predicted two-file conflict was resolved by keeping `0.36.0` and stacking both CHANGELOG entries with `## [0.35.4]` unmodified (`--check-preserved` confirms all 107 headings survive). - #3116 — workstream C, merged as `af3fea480`. C makes `source-control` drop 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 on `source-control`. - Audit findings are sealed in a `plugin-quality` evidence packet; `evidence-3.md` re-scopes F5 and `evidence-2.md` corrects a tally, both applied here. 🤖 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 8 actionable findings from the plugin-quality audit of
claude-ops:plugins(syncaction) recorded in #3112: the Step 2 silent no-op (F1), converge/enable commands routed into deleted directories (F2),sync allcovering one marketplace (F4), the unreported mid-run self-update (F6), the misstatedpluginConfigsread scopes (F7), the non-durable version capture (F12), the 61-call no-op update sweep (catalog-version pre-filter), and theversionsMatchfilter rule hand-maintained in four files (recurring-concerns §4). Bumpsclaude-ops0.35.3 → 0.36.0.The remaining audit findings (3 low-medium, 5 low) are sealed in the off-repo evidence packet (
audit-notes.md, memory-tier on the audited machine) and are not enumerated in the issue body, so they are deliberately not addressed here.Fix
Two commits, script mechanics first, then the doc surfaces that consume them:
294ef439—fleet-state.shgains four capabilities (each covered infleet-state.test.sh): a top-levelproject_rootfield (string | null) on every successful marketplace block, disambiguating "no project context" from "project context, zero in-repo installs" (F1's mechanism);projectPathExists(true/false/null) oninstalled[]records anddivergences[].scopes[]entries via a directory-existence probe (F2's mechanism); a standalone--marketplacesflag emitting CR-free marketplace names for per-marketplace iteration (F4's mechanism); and a fail-open--ids stale-userselector comparing user-scope installed versions against the local marketplace checkout's per-plugin manifests — an id is omitted only when its catalog version was read successfully and matches (the pre-filter's mechanism).c8ceeb0c— the doc surfaces consume them: sync.md Step 2 branches onproject_rootand SKILL.md's report gains an always-presentIn-repo:row (F1); every(cd "<projectPath>" && …)site gates onprojectPathExists, with orphaned install records as a named report-only category and all-orphaned divergences marked non-convergeable (F2);sync alliterates Steps 2–5 per marketplace via--marketplaces(F4); a conditionalNote:row states a self-updated run executed the pre-update algorithm (F6); thepluginConfigssentence now names user/--settings/managed as the read scopes since CC v2.1.207, contrasted withenabledPlugins(F7); version capture mandates amktempledger appended per mutating call (F12); Step 3 sweeps--ids stale-userwith the fail-open contract documented (48/61 exact prediction on the audited CC 2.1.240 run); and theversionsMatchfilter rule is normative only in scope-semantics.md, with SKILL.md/converge.md/gotchas.md pointing at it.All new or changed harness claims carry CLI version stamps attributed to the audited CC 2.1.240 run, per the issue's constraints; no call site hand-writes a
jqid extraction.Verification
fleet-state.test.sh: 45 → 56 cases, 0 failed (run main-side after each phase). New cases cover theprojectPathExiststri-state,project_rootstring/null/--allshapes,--marketplacesenumeration + CRLF regression + combination rejections, and everystale-userfail-open edge (missing installLocation, missing/invalid manifest, non-string version, object source, current-id omission, project-scope exclusion). No pre-existing case modified.shellcheckandshfmt -d: clean on both scripts.markdownlint-cli2: 0 issues across the six changed markdown files.typos,editorconfig-checker: clean on all changed files.--marketplacesemitsmelodic-software(exit 0) and rejects--all(exit 2); the block carriesproject_rootandprojectPathExists;--ids stale-useremits 0 ids on this fresh, all-current install.Related
plugins/plugin-quality/skills/audit/references/recurring-concerns.md§4 (SSOT/drift) — the concern that drove theversionsMatchconsolidation.claudeCLI verb reaps an install record whoseprojectPathno longer exists (observed on CC 2.1.240;prune -s projectshares the no-path-flag limitation).Generated by Claude Code