Skip to content

fix(claude-ops): emit plugin ids from fleet-state.sh instead of a hand-written jq loop (#2578) - #2581

Merged
kyle-sexton merged 6 commits into
mainfrom
fix/jq-crlf-windows
Aug 14, 2026
Merged

fix(claude-ops): emit plugin ids from fleet-state.sh instead of a hand-written jq loop (#2578)#2581
kyle-sexton merged 6 commits into
mainfrom
fix/jq-crlf-windows

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Contributor

Summary

plugins' sync steps told the reader to take an id list out of fleet-state.sh's JSON and loop a
claude plugin call over it, but never supplied the extraction — so every reader hand-wrote their
own jq -r. On Windows that hand-written jq reintroduces a CR that fleet-state.sh is careful to
strip, and the sweep then fails for every id but the last. Observed live: 64 of 65 user-scope
updates failed
, all with Plugin "<name>" not found — text identical to the documented bare-name
gotcha, so it reads as "the marketplace is broken".

This adds fleet-state.sh --ids <selector>, which emits the id list directly (CR-free by
construction), points Steps 2-5 at it, and corrects the CRLF mechanism gotchas.md describes.

Related

Closes #2578.

The mechanism, corrected

The native Windows jq opens stdout in text mode, so every \n it writes becomes \r\n.
Which capture is actually corrupted depends on how it is read — verified on jq 1.8.2 (winget
jqlang.jq) with MSYS bash 5.3.9:

Read form Result
x=$(… ), single-line output clean$(…) strips the trailing \r\n as a unit
x=$(… ), multi-line output every line but the last carries \r
mapfile -t / readarray -t every element carries \r, last included
jq output read back by jq self-cleaning — jq's stdin is text-mode too

Two of those contradict what gotchas.md said, and the corrections matter:

  • It claimed a single-line capture retains the \r. It does not, and that claim predicts the wrong
    symptom — all ids failing rather than all but the last. The all-but-last pattern is the single
    most useful diagnostic signal here (if only the last item in a batch worked, stop looking for a
    logic bug), and the old text talked the reader out of it.
  • jq→jq relays are self-cleaning, which narrows the hazard considerably: it is not "any jq output",
    it is jq's line output reaching a non-jq consumer — an external command's argv, a string
    comparison, a file write. That is exactly the shape the production loop had.

IFS=$'\n' does not help in any case: \r is not the separator, it rides inside the token.

Repo-wide sweep (item 2)

Scripted, paren-aware scan of all 499 tracked .sh files and 1120 .md files for jq output
reaching a line-wise consumer — jq | while read, < <(jq), mapfile/readarray, for x in $(jq),
and VAR=$(jq …) later split via <<<"$VAR". 31 shell sites and 4 markdown fenced blocks; 0
unguarded.
(Rows below group sites that share a file or a synced source; the per-row counts sum to
31 + 4.)

Site Shape Verdict
lib/hook-utils.sh:935 + 16 synced plugin copies read -d '' < <(jq -j) Guardedclean="${v//$'\r'/}" after the read, with a comment naming this exact hazard. One source, 17 files.
scripts/check-hook-exec-form.sh:282 while read < <(jq -r) Guardedrel="${rel%$'\r'}" in the loop body
scripts/check-hook-userconfig-argv.sh:113 while read < <(jq -r) Guarded — same in-loop strip
.claude/hooks/hook-telemetry-sink.sh:42 + plugins/claude-ops/hooks/ copy mapfile < <(jq) Guarded| tr -d '\r' in the pipeline
plugins/claude-ops/.../fleet-state.sh:591 while read <<<"$names" Guarded — file-scoped jq() wrapper (line 105)
plugins/claude-config/.../check-plugin-drift.sh:270 while read <<<"$var" Guarded| tr -d '\r'
plugins/work-items/.../claim.sh:102, lib/lease.sh:54 while read < <(jq -c) Benign — jq→jq relay; row is only ever re-parsed by jq, and the scalars derived from it are single-line $() captures
plugins/work-items/scripts/backfill-capability-tier-labels.sh:90 jq -c | while read Benign — same relay. body reaches grep -E with unanchored patterns, so a trailing CR cannot change the match
plugins/source-control/.../fetch-annotations.sh:128 while read <<<"$var" Benign — relay; cr_id/cr_name are single-line captures
plugins/claude-config/.../fix-plugin-drift.sh:125,134 (2) while read <<<"$var" Benign — ids go to jq -nR '[inputs]', which strips the CR on input. Reproduced end-to-end: settings.json keys come out clean and the orphan del() succeeds
plugins/claude-config/.../fix-plugin-drift.sh:140,146 (2) while read <<<"$var" Benign — display-only printf
4 markdown fenced shell blocks (wayfind/tracker-mechanics.md, babysit-loop, attend-queue, work-loop) --jq | while read Guarded — all four already carry | tr -d '\r'

fix-plugin-drift.sh was the one site that looked genuinely broken — it builds
"\(.name)@\(.marketplace)" ids, the exact shape from the incident, and feeds them into a
read-modify-write of the user's settings.json. It was worth reproducing rather than reasoning
about, and the reproduction is what surfaced jq's input-side text-mode translation: the CR is
stripped again by jq -nR, del() matches, and nothing corrupt is written. Classified benign on
evidence, not inspection.

Remediation chosen, and why

Eliminate the reader-side jq rather than document a guard around it. Every guard style already
in this repo (| tr -d '\r', ${v//$'\r'/}, ${var%$'\r'}, the file-scoped jq() wrapper) only
helps code that is in the repo. The failure here was in shell an agent improvised from prose, so
no guard convention could have reached it. --ids removes the improvisation surface: the step is
now a read loop over a script that already routes every jq call through its wrapper.

Rejected alternatives: adding | tr -d '\r' to the prose (still hand-written, still forgettable,
and only fixes Step 3 of four); IFS=$'\n' (verified not to help); jq --raw-output0 + read -d ''
(works, but would introduce a second idiom next to the tr -d '\r' convention already established
across 20+ sites).

Red-first evidence (item 4)

New assertions run against the unmodified fleet-state.sh (pre-fix script + new tests, in the
worktree so hook-utils.sh still resolves):

41 cases, 8 failed
FAIL: --ids installed-user: exit 0
FAIL: --ids installed-user: one fully-qualified id per line
FAIL: --ids missing-user-install: catalog entry installed nowhere
FAIL: --ids CR regression: exit 0 under a CRLF-emitting jq
FAIL: --ids CR regression: ids are byte-exact under a CRLF-emitting jq
FAIL: --ids unknown selector: names the bad selector
FAIL: --ids with no selector: says a selector is required
FAIL: --ids with --all: refuses rather than inventing a shape

Every pre-existing case stayed green, so the 8 failures are the new contract and nothing else.

The CR case is host-independent. A PATH stub named jq normalizes every emitted line to exactly
one trailing CR (sed 's/\r*$/\r/'), so it exercises the same bytes on a Linux runner (where real
jq emits bare LF and the stub adds the CR) as on Windows (where the stub is a no-op). command jq
resolves through PATH, so fleet-state.sh's own wrapper calls the stub exactly as it would call a
native Windows jq — meaning the case goes red both if --ids is removed and if that wrapper is
ever deleted.

Two things learned from #2571's lesson and applied here:

  • A stub probe asserts the stub really emits CRLF, so a stub that quietly stopped working cannot
    make the CR assertion vacuously pass.
  • The CR assertion has an explicit empty-output branch. Against the pre-fix script the output
    was empty and *$'\r'* was vacuously false — the assertion passed for the wrong reason until that
    branch was added. That is the same "test encodes the bug as the contract" failure feat(ci): gate exec-form hooks against bare command names (#2569) #2571 fixed, and
    it showed up here in the first red run.

Should this be a CI lint? (item 6) — No

Not in #2569/#2571's gate: check-hook-exec-form.sh is exec-form/bare-command-name domain, and a CR
class shares none of its parsing. The only sane home would be scripts/check-shell-portability.sh
(changed-.sh lint, data-driven token list, an existing !name mechanism for classes needing code
rather than an ERE, and a per-site portability-ok: escape). Even so, it should not be added now:

  1. It would not have caught this incident. The failing shell was never a tracked file. Every
    gate in this repo reads the repo.
  2. There is nothing to catch. 29/29 sites already guarded or benign; a lint shipping against
    zero violations is pure false-positive risk on unrelated PRs.
  3. The analysis it needs does not fit the gate. Guards legitimately appear in four forms and
    often in the loop body, after the redirect that names jqcheck-shell-portability.sh's
    line-record model has no loop-scope view, and every existing class is decidable within a record.

If an unguarded site ever does land, check-shell-portability.sh is the home and this section is
the design note. Filing a follow-up issue for a lint I am arguing against would just park the same
false-positive risk in the backlog.

Changes

  • plugins/claude-ops/skills/plugins/scripts/fleet-state.sh--ids <selector> (installed-user,
    current-project, missing-user-install, missing-enabled). One tab-separated record per line,
    first field always the fully-qualified id; current-project carries scope as a second field.
    Selector validated at parse time; refuses --all; per-marketplace failure blocks go to stderr in
    this mode. Header documents the record shape and the widened exit-2 set.
  • plugins/claude-ops/skills/plugins/scripts/fleet-state.test.sh — 22 assertions across 10 cases,
    incl. the host-independent CR regression, the dual-scope pairing case, and the
    error-block-off-stdout case.
  • context/sync.md — Steps 2-5 cite --ids; Step 3 carries the full guarded loop.
  • context/gotchas.md — mechanism corrected (per-read-form table, all-but-last signature, jq→jq
    self-cleaning, mapfile caveat) and pointed at --ids. No second CR section added.
  • CHANGELOG.md / plugin.json — 0.31.15.

Verification

Gate Result
fleet-state.test.sh (Windows, real native jq) 45 cases, 0 failed
scripts/validate-plugins.sh pass
scripts/check-changelog-parity.sh --check / --check-bump origin/main pass
shellcheck (both changed .sh) clean
markdownlint-cli2 (all changed .md) 0 errors
scripts/check-shell-portability.sh --paths no unexcused constructs
scripts/check-skill-portability.sh --paths no unexcused tokens

The hook-utils-windows lane covers lib/hook-utils.test.sh; this change is claude-ops-scoped, so
the meaningful Windows evidence is the local run above against the real native jq — the exact binary
whose text-mode stdout causes the defect.

@cursor

cursor Bot commented Aug 14, 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 14, 2026

Copy link
Copy Markdown
Contributor

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


Code review complete

  • Load /review:code-review skill instructions
  • Gather context (diff, changed files, CLAUDE.md / REVIEW.md guidelines)
  • Review fleet-state.sh changes for correctness
  • Review fleet-state.test.sh for coverage
  • Review sync.md / gotchas.md doc changes
  • Review CHANGELOG.md / plugin.json version bump
  • Post findings as inline comments and final summary

Findings

1 inline comment postedsync.md Step 2 (--ids current-project) discards the scope needed to pair id lines back to -s project/-s local, and the selector has no test coverage. Summary: the current-project selector's jq filter projects only .id, so a plugin installed at both project and local scope for the same repo (the exact multi-scope case divergences[] tracks) emits the same id twice with no way to tell which line needs which -s flag — Step 2's doc text doesn't resolve this ambiguity the way Step 3-5's full loop code does, and no test in fleet-state.test.sh exercises current-project at all (let alone the dual-scope fixture).

What I checked and didn't flag

  • emit_marketplace/emit_one/emit_ids exit-code propagation (emit_one "$TARGET" || exit $?) — traced through the --ids-with-unknown-selector, --ids-with-no-selector, and --ids-with---all paths; all match the documented exit-code contract and the new tests.
  • The CR-regression test's PATH-stub approach (host-independent sed 's/\r*$/\r/' normalization) and the empty-output edge case it guards against — sound, and a good callback to the feat(ci): gate exec-form hooks against bare command names (#2569) #2571 lesson it cites.
  • sync.md/gotchas.md prose edits for Steps 3-5 and the corrected CRLF mechanism table — consistent with the script and internally coherent.
  • CHANGELOG.md/plugin.json version bump — consistent with Added/Fixed/Changed entries.

I did not independently execute the shell test suite, shellcheck, or markdownlint (this review lane doesn't have general Bash execution) — the verification table in the PR description is author-claimed and unverified by me.

@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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


Security Review — /review:security-review

  • Gather context (PR diff, description, changed files)
  • Run /review:security-review procedure
  • Analyze changed files for security issues
  • Post findings

No security issues found. Skip-gate condition 2 applies: this change has no security-relevant surface.

What changed: fleet-state.sh gains an --ids <selector> mode (fleet-state.sh#L555-L624) that projects a marketplace JSON block down to a plain id list, plus corresponding doc updates (sync.md, gotchas.md) and tests. No GitHub Actions workflow files, auth/access-control paths, or network-facing code are touched.

Specifically checked and ruled out:

  • jq filter injection in emit_ids (fleet-state.sh#L568-L579): the $selector argument only ever selects among four fixed, hardcoded jq filter literals via a case statement; an unmatched selector hits the *) branch and errors out. There is no path from --ids <arbitrary> into an interpolated jq program.
  • Herestring data flow (jq -r "$filter" <<<"$block"): $block is JSON data passed as jq's input, not shell-evaluated, so no command injection there.
  • The pre-existing CR-stripping wrapper (jq() { command jq "$@" | tr -d '\r'; }, fleet-state.sh#L118) and the hook-utils.sh sourcing guard (script-relative path, not env-overridable) are both unmodified by this PR.
  • Ids emitted flow only into locally-run claude plugin update <id> calls, sourced from the user's own local installed_plugins.json/marketplace catalog — no remote or attacker-controlled input reaches this path.
  • Test file changes (fleet-state.test.sh) build a PATH-stub jq for CR-regression testing using command -v jq (trusted local path), not attacker-controlled input.
  • plugin.json/CHANGELOG.md changes are a version bump and changelog entry only.

This is a correctness/documentation fix for a Windows CRLF bug in local dev tooling, with no trust-boundary, injection, or credential-handling surface introduced.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

Last security-reviewed head: 006a02122c5d04f60871ac240e881a6d9935dcdf. 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: 136c680e5b

ℹ️ 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/scripts/fleet-state.sh Outdated
Comment thread plugins/claude-ops/skills/plugins/context/sync.md Outdated
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

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

kyle-sexton added a commit that referenced this pull request Aug 14, 2026
…with each id (#2578)

Review findings on #2581.

Error objects out of the id stream: emit_marketplace writes a
{marketplace:{name,error}} block to stdout and returns nonzero, but the
documented consumer is `while read ... done < <(... --ids ...)`, and a process
substitution does not propagate its command's exit status. The block was read
as an id and handed to `claude plugin update` verbatim. Under --ids that block
now goes to stderr; report mode is unchanged.

Scope pairing: `--ids current-project` projected only .id, but one plugin can
hold BOTH a project- and a local-scope record for the same repo, each
currentProject:true. The id appeared twice with nothing to distinguish the
lines, so Step 2 could not pick the right `-s` flag and a `sort -u` would drop
an update. Records are now tab-separated with the id always first;
current-project carries scope as a second field. Step 2 reads it with
`IFS=$'\t' read -r id scope`.

Also hoists selector validation to parse time, so a bad selector reports as a
usage error (exit 2) instead of being masked by an unresolvable marketplace's
exit 1, and adds `set -o pipefail` to the test's jq stub so it reports jq's
status rather than sed's.

New cases: dual-scope pairing, error-block-off-stdout, bad-selector-outranks-
unresolvable-marketplace, and --ids on the zero-arg default path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac8xjCjMxFsXGHCXKHY4W
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 5s —— View job


Code review complete

  • Load /review:code-review skill instructions
  • Gather context (diff, changed files, CLAUDE.md / REVIEW.md guidelines)
  • Review fleet-state.sh changes for correctness
  • Review fleet-state.test.sh for coverage
  • Review sync.md / gotchas.md doc changes
  • Review CHANGELOG.md / plugin.json version bump
  • Post findings as inline comments and final summary

Scope of this pass

This is a synchronize re-review. The prior head (136c680) already got a full code-review and security-review pass. The new commit (ae07eef) is a direct, scoped response to the two review findings posted against 136c680:

  • Codex's finding that a --ids failure block could leak onto stdout and be mistaken for an id — fixed by sending it to stderr under --ids only (fleet-state.sh emit_one, L638-646).
  • My own finding that --ids current-project couldn't distinguish a dual-scope (project + local) install — fixed by making that selector emit tab-separated id\tscope records, with sync.md Step 2 updated to a real IFS=$'\t' read loop (sync.md L96-108).

I re-reviewed both fixes end-to-end rather than trusting the description:

  • Error-off-stdout fix: correctly scoped to the IDS_SELECTOR branch only — report mode is untouched, matching the documented output contract. New test case (error-block-off-stdout) asserts stdout is empty under --ids on a broken catalog and that the block still appears on stdout in report mode. Looks right.
  • Scope-pairing fix: ids_selector_filter is now a single source of truth shared by parse-time validation and the actual filter (good — closes the drift risk between "what's rejected" and "what's emitted"). Selector validation was also hoisted before marketplace resolution, so a bad selector now reports exit 2 instead of being masked by an unrelated exit 1 — covered by the new bad-selector-outranks-unresolvable-marketplace case. The dual-scope test fixture (one plugin, both project and local records, both currentProject: true) is exactly the case that was previously ambiguous, and it now round-trips correctly through the documented IFS=$'\t' read loop.
  • Selector validation is checked twice (once at parse time in the main script, once defensively inside emit_ids via ids_selector_filter) — harmless, the second is unreachable given the first, not a correctness issue.

1 new inline comment posted — a stale leftover comment in emit_one (fleet-state.sh#L623-L627) that says the failure block is "passed through verbatim in BOTH modes," directly contradicting the correct, newer comment immediately below it and the actual stderr-under---ids code. Low risk (code and the newer comment agree), but it's the exact kind of two-comments-disagree confusion this PR eliminates elsewhere, worth deleting.

What else I checked and didn't flag

  • CHANGELOG.md Added/Fixed/Changed entries — updated consistently with the new tab-separated record shape and the stderr-routing change; no gaps against this commit's diff.
  • plugin.json version (0.31.15) — unchanged from the prior commit, correctly not re-bumped for this same-PR follow-up.
  • set -o pipefail added to the test's CRLF jq stub — correct fix for a real gap (without it, a future failure-path case added under that stub would pass vacuously since the stub piped through sed).
  • New --ids installed-user zero-arg-default-path case — good regression pin that --ids stays wired on both call sites (default and --marketplace), not just one.

I did not independently execute the shell test suite, shellcheck, or markdownlint (this lane doesn't have general Bash execution) — the verification table in the PR description is author-claimed and unverified by me.

@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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


Security Review — /review:security-review

  • Load /review:security-review skill instructions
  • Gather context (diff since last-reviewed head, changed files)
  • Analyze changed files for security issues
  • Post findings

No security issues found.

Scope of this pass: per the relevance-gate note above, this re-review covers only the commit pushed since the last successful security review (136c680): ae07eef — "keep error JSON off the --ids stream and pair scope with each id." That commit directly resolves the two review findings left on the prior head (Codex's error-JSON-on-stdout finding and my own current-project scope-pairing finding); nothing else changed.

What changed and why it's not security-relevant:

  • ids_selector_filter() centralizes the selector→jq-filter mapping used by both parse-time validation and emit_ids. All four filters remain fixed string literals selected via a case on $1; there is still no path from the --ids <selector> argument into an interpolated jq program — same conclusion as the base-diff review, just refactored into one function.
  • emit_one() now routes a per-marketplace failure block to stderr under --ids instead of stdout. This closes the reliability gap Codex flagged (a < <(… --ids …) consumer can't see the process's exit status, so an error JSON object left on stdout would previously be read as a plugin id and handed to claude plugin update verbatim). The block's content is local, non-attacker-controlled diagnostic text either way — this is a correctness/reliability fix, not a vulnerability, but it's a sound fix and worth confirming it didn't introduce a new leak path: it did not (still the same local stdout/stderr streams, no new sink).
  • current-project records now carry scope as a tab-separated second field (fleet-state.sh#L579), consumed in sync.md Step 2 via IFS=$'\t' read -r id scope; claude plugin update "$id" -s "$scope". Both id and scope originate from the user's own local installed_plugins.json/marketplace catalog (the same trust boundary already assessed on the base diff), and -s only ever takes the literal scope string already stored in that local record — no new externally-influenced input reaches an argv position.
  • Selector validation moved to parse time (before any marketplace resolution) purely to fix exit-code precedence (usage error 2 vs. resolution failure 1) — no behavioral change to what selectors are accepted.
  • fleet-state.test.sh additions are test-only, exercising the above with local fixtures.

No GitHub Actions workflow files, auth/access-control paths, or network-facing code are touched by this commit, consistent with the base-diff assessment.

Comment thread plugins/claude-ops/skills/plugins/scripts/fleet-state.sh Outdated
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 53s —— View job


Code review complete

  • Load /review:code-review skill instructions
  • Gather context (diff since last-reviewed head, changed files)
  • Review latest commit (a32d625) for correctness
  • Post findings as inline comments and final summary

Scope of this pass

This is a synchronize re-review. The prior two heads (136c680, ae07eef) already received full code-review and security-review passes, and both review findings from those passes (the error-JSON-on-stdout leak and the current-project scope-pairing gap) are resolved. The only new commit is a32d625: swap [ -n "$id" ] for [[ -n "$id" ]] in the three doc loop snippets (sync.md Steps 2 and 3, gotchas.md's corrected-mechanism example), to keep the copy-pasteable snippets consistent with this repo's own shellcheck SC2292 lint (which the test file was corrected for earlier in this PR).

What I checked

  • Confirmed all three snippet sites were updated (sync.md#L102, sync.md#L141, gotchas.md#L143) and no [ -n ...]/single-bracket test survives anywhere in the two doc files.
  • Checked Steps 4 and 5 of sync.md for similar loop snippets that might have been missed — neither contains a copy-pasteable loop, so there was nothing else to convert.
  • Cross-checked fleet-state.sh and fleet-state.test.sh: every conditional in both already uses [[ ]], so the doc snippets are now consistent with the actual scripts and with each other — no remaining [ ... ] single-bracket test anywhere in this PR's changed files.
  • Semantics are unchanged ([[ -n "$id" ]] vs [ -n "$id" ] behave identically here; this is purely a lint-convention fix), so no behavioral review was needed beyond confirming the substitution was applied correctly at each site.

No issues found — this commit is small, correct, and self-contained. No inline comments to post.

@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 34s —— View job


Security Review — /review:security-review

  • Load /review:security-review skill instructions
  • Gather context (diff since last-reviewed head, changed files)
  • Analyze changed files for security issues
  • Post findings

No security issues found.

Scope of this pass: per the relevance-gate note above, this re-review covers only the commit pushed since the last successful security review (ae07eef): a32d625 — "use [[ ]] in the plugins-skill loop snippets."

What changed: a purely cosmetic edit to two markdown files, swapping [ -n "$id" ] for [[ -n "$id" ]] in three documented while read loop snippets:

Why this is not security-relevant:

  • No .sh files, workflows, or executable code changed — only prose/example blocks in two markdown files.
  • [ ... ] and [[ ... ]] are semantically equivalent for this specific test (-n "$id" on a variable that is always quoted); this swap doesn't change what the guard accepts or rejects, doesn't alter data flow, and introduces no new input source.
  • The ids/scope flowing through these loops are unchanged from the base diff already reviewed (ae07eef) — sourced from the user's own local fleet-state.sh --ids ... output, fed only into claude plugin update "$id" -s "$scope" as literal argv values.
  • No GitHub Actions workflow files, auth/access-control paths, or network-facing code are touched.

This commit carries no security surface at all — it's a style-consistency fix to documentation examples.

@kyle-sexton kyle-sexton reopened this Aug 14, 2026
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

CI trigger note — not merge-ready until the pull_request lanes re-fire

Full CI last ran green on a32d625e: 38 SUCCESS / 1 SKIPPED / 0 failures, including
hook-utils-windows, plugin-gate, shell-portability-lint, skill-quality-gate, hygiene,
review, security-review, and pr-issue-linkage.

Since roughly 05:33 UTC no pull_request-triggered workflow has started anywhere in this repo
ci, claude-review, claude-security-review, pr-title, and link-check are all affected, on
this branch and repo-wide. Only the two pull_request_target workflows (do-not-merge,
pr-issue-linkage) still fire, and they pass. Tried: a normal push, close/reopen, and an empty
commit — none produced a pull_request run. All 13 workflows report state: active, so this looks
like an Actions-side trigger issue rather than repo configuration.

The three commits after a32d625e are deliberately low-risk, and each was verified locally:

Commit Change
4e955c90 deletes 5 stale comment lines in emit_one (review finding)
391eade2 adds 7 documentation lines to SKILL.md
59c387d8 empty commit, CI retrigger attempt

Local verification on the current head (59c387d8), Windows / Git Bash with the real native jq:

Gate Result
fleet-state.test.sh 45 cases, 0 failed
shellcheck (both changed .sh) clean
markdownlint-cli2 (all 4 changed .md) 0 errors
scripts/validate-plugins.sh pass
check-changelog-parity.sh --check / --check-bump / --check-preserved pass
check-shell-portability.sh origin/main no unexcused constructs
check-skill-portability.sh origin/main no unexcused tokens
check-silent-skips.sh / check-discriminating-test-skips.sh pass

Please re-run CI before merging rather than relying on the a32d625e result — I am not treating
this as green.

kyle-sexton and others added 6 commits August 14, 2026 02:12
…d-written jq loop (#2578)

`sync` Steps 2-5 told the reader to take an id list out of fleet-state.sh's
JSON and loop a `claude plugin` call over it, but never supplied the
extraction, so every reader hand-wrote their own `jq -r`. The native Windows
jq writes stdout in text mode and `$(...)` strips only the trailing CRLF, so
every id but the last reached the CLI as `<name>@<marketplace>\r` and failed
with `Plugin "<name>" not found` -- text identical to the bare-name gotcha,
which misdirects the diagnosis. Observed live: 64/65 updates failed.

Adds `fleet-state.sh --ids <selector>`, emitting one fully-qualified id per
line, CR-free by construction via the wrapper the script already has, so the
sweep needs no reader-side jq at all. Steps 2-5 now cite it.

Also corrects the mechanism gotchas.md described: a single-line capture is
clean (not corrupt as claimed), only multi-line output keeps a CR on every
line but the last, `mapfile -t` has no last-element reprieve, and jq->jq
relays self-clean because jq's stdin is text-mode too.

Regression test is host-independent: a PATH stub normalizes jq's output to
exactly one trailing CR, so it exercises the same bytes on Linux as on
Windows, and goes red both if --ids is removed and if the wrapper is deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac8xjCjMxFsXGHCXKHY4W
…with each id (#2578)

Review findings on #2581.

Error objects out of the id stream: emit_marketplace writes a
{marketplace:{name,error}} block to stdout and returns nonzero, but the
documented consumer is `while read ... done < <(... --ids ...)`, and a process
substitution does not propagate its command's exit status. The block was read
as an id and handed to `claude plugin update` verbatim. Under --ids that block
now goes to stderr; report mode is unchanged.

Scope pairing: `--ids current-project` projected only .id, but one plugin can
hold BOTH a project- and a local-scope record for the same repo, each
currentProject:true. The id appeared twice with nothing to distinguish the
lines, so Step 2 could not pick the right `-s` flag and a `sort -u` would drop
an update. Records are now tab-separated with the id always first;
current-project carries scope as a second field. Step 2 reads it with
`IFS=$'\t' read -r id scope`.

Also hoists selector validation to parse time, so a bad selector reports as a
usage error (exit 2) instead of being masked by an unresolvable marketplace's
exit 1, and adds `set -o pipefail` to the test's jq stub so it reports jq's
status rather than sed's.

New cases: dual-scope pairing, error-block-off-stdout, bad-selector-outranks-
unresolvable-marketplace, and --ids on the zero-arg default path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac8xjCjMxFsXGHCXKHY4W
The doc snippets used `[ -n "$id" ]`, which shellcheck flags as SC2292 in this
repo's own lint. A reader copying the Step 2/3 or gotchas loop into a .sh file
would trip the rule the test file was just corrected for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac8xjCjMxFsXGHCXKHY4W
The --ids stderr routing added in ae07eef left the previous paragraph in
place next to its replacement, so two adjacent comments made opposite claims
about where a per-marketplace failure block goes. Keeps only the paragraph
that matches the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac8xjCjMxFsXGHCXKHY4W
…2578)

The state-inspection block showed only the JSON-report form, so a reader who
never opens sync.md still has no signal that an id list is available and
hand-writes a jq extraction — the exact path this fix exists to close.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac8xjCjMxFsXGHCXKHY4W
The pull_request-triggered workflows stopped firing repo-wide after 05:33 UTC;
this commit carries no content change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac8xjCjMxFsXGHCXKHY4W
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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


Code review complete

  • Load /review:code-review skill instructions
  • Gather context (diff since last-reviewed head, changed files)
  • Review latest commits for correctness
  • Post findings as inline comments and final summary

Scope of this pass

This is a synchronize re-review at head 006a021 (006a02122c5d04f60871ac240e881a6d9935dcdf). Note the branch history was rewritten between the last review pass and this one — the SHAs referenced in earlier review comments (136c680, ae07eef, a32d625e, 4e955c90, 391eade2, 59c387d8) no longer exist, but their content maps 1:1 onto the current log (fa15ee5, 6c89658, 22f67de, b8abad1, f3fdc3e, 006a021 respectively) — confirmed by diffing each against its predecessor. Nothing was lost or altered in the rewrite.

Of those, only two carry content not yet independently reviewed by Claude (the prior passes covered up through a32d625e / 22f67de):

  • b8abad1 — deletes the stale, contradictory emit_one comment paragraph (my own finding from the prior pass). Diff confirmed: removes exactly the outdated "passed through verbatim in BOTH modes" paragraph, leaves the correct stderr-under---ids paragraph intact. Correct.
  • f3fdc3e — adds the --ids <selector> usage line to SKILL.md's state-inspection block, with a pointer to gotchas.md. Consistent with the --ids contract described in fleet-state.sh and sync.md (tab-separated, id-first, CR-free); doesn't need its own test since it's prose only.
  • 006a021 ("chore: retrigger CI") — confirmed empty, no content change.

What I checked on this pass

  • Re-read emit_one/emit_ids/ids_selector_filter in fleet-state.sh end-to-end against the current head — single source of truth for selector validation, stderr-routing under --ids scoped correctly to the IDS_SELECTOR branch only, current-project's id\tscope tab-record shape matches what sync.md Step 2's IFS=$'\t' read loop expects.
  • Confirmed fleet-state.test.sh has a current-project dual-scope case (dual-scope install keeps each scope on its own record, Step 2's IFS=tab read recovers both id/scope pairs) and an error-off-stdout case — both gaps flagged in earlier passes are covered.
  • CHANGELOG.md/plugin.json (0.32.1) — Added/Fixed/Changed entries match the final --ids contract, stderr routing, and corrected CRLF mechanism; version bump is not double-counted across the doc-only follow-up commits.
  • git status clean, no uncommitted changes.

I was not able to execute the shell test suite, shellcheck, or markdownlint in this lane (no general Bash execution permission here) — the verification tables in the PR description and the author's latest comment (45 cases / 0 failed, clean shellcheck/markdownlint, on Windows with real native jq) are author-claimed and unverified by me, same limitation as the prior passes.

No new issues found. No inline comments to post — this pass is a clean confirmation that the two prior review findings were fixed correctly and nothing regressed.

@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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


Security Review — /review:security-review

  • Load /review:security-review skill instructions
  • Gather context (diff since last-reviewed head, changed files)
  • Analyze changed files for security issues
  • Post findings

No security issues found.

Scope of this pass: per the relevance-gate note above, this re-review covers only the commits pushed since the last successful security review (a32d625e): 4e955c90 (delete stale emit_one comment), 391eade2 (add --ids usage docs to SKILL.md), and the two empty CI-retrigger commits (59c387d8, 006a021). Nothing else changed on this head (006a021).

What changed and why it's not security-relevant:

  • emit_one() in fleet-state.sh — deletes a stale, pre-IDS_SELECTOR comment paragraph that contradicted the adjacent (correct) comment and the actual stderr-under---ids behavior reviewed on ae07eef. Comment-only change; no code, control flow, or data flow touched.
  • SKILL.md — adds seven documentation lines showing the --ids <selector> usage form and pointing at context/gotchas.md. Pure prose; no executable content, no new command form introduced (the flag itself was already reviewed on 136c680/ae07eef).
  • The two empty commits (59c387d8, 006a021) carry no diff at all — CI-retrigger attempts per the author's comment above, following repeated pull_request workflow trigger failures.

No .sh files gained new logic, no workflow files changed, and no new input source, trust boundary, or argv-construction path was introduced by this pass. This is consistent with the conclusions already reached on 136c680 and ae07eef — the --ids selector mapping remains a fixed case over four literal jq filters, and ids/scope still flow only into local claude plugin update/install/enable calls sourced from the user's own local catalog.

@kyle-sexton
kyle-sexton merged commit dec5c29 into main Aug 14, 2026
39 checks passed
@kyle-sexton
kyle-sexton deleted the fix/jq-crlf-windows branch August 14, 2026 06:25
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 sync teaches an unguarded jq loop; Windows CR breaks every id but the last

1 participant