Skip to content

fix(claude-ops): key lane markers in the target repo, and scope stop-gate arming to the installs that asked - #2050

Merged
kyle-sexton merged 12 commits into
mainfrom
fix/claude-ops-lane-launcher-hardening
Aug 9, 2026
Merged

fix(claude-ops): key lane markers in the target repo, and scope stop-gate arming to the installs that asked#2050
kyle-sexton merged 12 commits into
mainfrom
fix/claude-ops-lane-launcher-hardening

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Contributor

Three bot-filed P2 defects in the claude-ops lanes launcher, all verified to reproduce at origin/main and to stop reproducing here. Every fix is covered by a new assertion that FAILS against the pre-fix launcher and passes against this one.

1. The launch-commit marker key was digested in the wrong repository (#1383)

git hash-object uses the object format of whatever repository it resolves. The launcher called it unscoped, so it keyed on the caller's format while taking the toplevel from the repository --repo names. Reached from a SHA-1 working directory, a SHA-256 target produced a 40-character key, while skills/lanes/context/refresh.md's probe runs inside that checkout and computed the 64-character one — the launcher wrote its marker to a directory the probe never reads and staleness detection was silently off.

The comment above the key asserted the two sides agree because both call git rev-parse --show-toplevel. That settles the path and says nothing about the digest, so the invariant it claimed did not hold.

Both digests are now taken with -C "$REPO". The anchor is $REPO (guaranteed by resolve_repo to be an existing directory) rather than the hashed $top (a string git handed back) — anchoring on a path that may not exist would fail the digest into the unkeyed fallback and collapse every such repo onto one key.

Scope is broader than the filed report: restart-consumer.sh derived its ledger key the same unscoped way and is fixed with it. The hand-recompute snippets in the README, the changelog, and refresh.md already run inside the target repository and were correct as written; they are untouched.

2. An explicitly empty stop-gate marker was read as an absent one (#1865)

gate_option_from_settings ended select(type == "string") ] | last // empty, which prints nothing for an explicit "" and nothing for an absent key. [[ -n "$marker" ]] then dropped --marker for both, the arm record carried no marker key at all, and lane-stop-gate.sh's precedence (managed ▷ arm record ▷ user settings ▷ default) walked past it to the user-level marker — where a marker file left over from another lane can authorize a stop this lane never signaled.

A v: prefix now carries "the lane set this" through the shell, so an explicit empty value reaches the helper as --marker "", lane-stop-gate-arm.sh records {"marker": ""}, and the gate's [[ -n "$MARKER" ]] guard leaves the marker channel off instead of falling through.

The sentinel is deliberately not symmetric. lane-stop-gate.sh substitutes the default token for an empty sentinel, so emptiness is not a configured value there; recording one would buy no behavior change while shadowing the user-level sentinel. An empty sentinel is therefore still treated as absent, and a fixture pins that asymmetry.

3. The stop-gate arm id reached installs that never asked for it (#1865)

Arming keyed off an any-quantifier over the autonomy / autonomy@* namespace, then injected lane_stop_gate_arm_id into every entry in it, and option extraction took its last match from any entry rather than a requesting one.

The gate never treats this channel as a trusted verdict in either direction, so an id landing on an entry set to false was not overriding that false. What it did do is mark installs the lane never asked to arm — leaving the settings handed to claude an inaccurate record of what was requested, and letting a non-requesting entry's marker reach the arm call. One shared filter now defines "an entry that requested the gate", and detection, option extraction, and injection all use it.

Arming every discovered helper script is unchanged and deliberate.

Tests

lane-launcher.test.sh grows a SHA-256 cross-format marker fixture (skipped where git cannot create a SHA-256 repository) and four gate-arming fixtures. Against the pre-fix launcher with this test file, six assertions fail:

  • marker: written under the TARGET repo's object-format key
  • marker: nothing is written under the caller-format key
  • arm: an explicitly empty marker still reaches the helper
  • arm: options come from the requesting entry
  • arm: a disabled sibling's marker never reaches the helper
  • arm: the explicitly-disabled entry receives no arm id

All six pass here; the suite is 193 assertions, 0 failures.

Verification (independent re-run)

The pre-fix control was reproduced by copying the scripts directory to a scratch path, replacing
lane-launcher.sh with origin/main's, and running this branch's unchanged test file against
it: lane-launcher.test: FAIL — 6 case(s) failed there, PASS — 193 cases here. The six failures
are exactly the list above, so no fixture is passing on both trees.

The SHA-256 block executed rather than skipping — git version 2.54.0.windows.1 creates
--object-format=sha256 repositories, and cases 158-160 report PASS. The skip guard remains because
the format is not universally compiled in.

Beyond the arm stub: lane-stop-gate-arm.sh invoked directly with --marker "" writes
"marker": "" into the arm record, while omitting the flag writes no marker key at all. The
launcher's explicit-empty distinction therefore survives to the gate, whose gate_option returns the
empty string (via the same v: idiom) rather than falling through to user settings.

Folding the key test and the value test into one select adds no type fragility: jq's and
short-circuits, so a non-autonomy scalar entry is never indexed, and an autonomy entry whose value
is a scalar errors identically under the old and new filters.

Gates from the worktree root, all green: check-changelog-parity.sh --check / --check-bump origin/main / --check-order, check-shell-portability.sh, check-skill-portability.sh,
check-silent-skips.sh, check-plugin-manifest-presence.sh, check-changed-skills.sh origin/main
(0 errors; one pre-existing SKILL.md-length warning), validate-plugins.sh, markdownlint-cli2 on
the changelog, and shellcheck -x on all three changed scripts.

Version renumbered to 0.27.6main published 0.27.4 and then 0.27.5 while this branch was in
flight.

Related

No linked issue

The three findings were filed as review threads on merged PRs #1383 and #1865, not as issues. Those PRs are referenced for provenance only — this PR closes nothing.

A fourth thread on #1383, PRRT_kwDOTCGFQM6TzlNw, needs no change here: the vacuous-traversal escape
it describes was already closed by 353baf64 (#1851), and a control at 353baf64^ reproduces it.
Its adjacent defence-in-depth observation — the three preflight jq substitutions in
lane-launcher.sh that ignore exit status — is deliberately left for a separate change. Lines 1-406
of that file are byte-identical to main and all seven $(jq …) command substitutions in it are
unchanged; the only edit above the marker-key block is line 407, where the property list's own count
went from "Two" to "Three".

kyle-sexton and others added 6 commits August 7, 2026 08:28
Unverified batch preserved from a session that ended before its findings were
falsified against origin/main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops two changes that no filed finding motivates and repairs the fixture
that was meant to pin the object-format key.

resolve_config returns to its state on main. The per-query failure guard it
had grown is a real gap, but a separate one — the launcher's marker keying,
its arm-option plumbing, and its arm-id injection are all downstream of a
config that already parsed, so nothing here depends on it.

An explicitly empty sentinel is treated as absent again, matching main. The
marker and the sentinel are not symmetric at the gate: an empty marker turns
that channel off, while an empty sentinel is replaced by the default token,
so recording one would change only which source the gate consults — it would
shadow the user-level sentinel — with no behavior to show for it.

The SHA-256 fixture never built a SHA-256 repository. The suite's `git` PATH
stub answers pull, rev-parse, and hash-object and exits 0 for everything
else, so `git init --object-format=sha256` reported success while creating
nothing, and the fixture then compared two SHA-1 keys — it failed on the
fixed launcher and would have kept failing. It now calls the real git.

The multi-install fixture put the non-requesting entry first, where the
last-match option read picks the requesting entry's marker either way. The
non-requesting entry is now last, so the assertion distinguishes a scoped
read from an unscoped one.

Comment prose no longer says an arm id overrides an entry's own `false`. The
gate does not trust this channel in either direction; the defect is that the
launcher marks an install the lane never asked to arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ane fixes

Records the repo-marker key scoping, the explicit-empty marker distinction, and
the per-entry arm-id injection the branch already carries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

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

Copy link
Copy Markdown

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


Security review complete

  • Gather context (PR diff, changed files)
  • Review lane-launcher.sh changes for injection / logic flaws
  • Review restart-consumer.sh changes
  • Review lane-launcher.test.sh changes (test-only, sanity check)
  • Review plugin.json / CHANGELOG.md (non-security, skip deep review)
  • Post findings

Scope: all 5 files changed in this PR (plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh, restart-consumer.sh, lane-launcher.test.sh, plugin.json, CHANGELOG.md), diffed against origin/main.

No security issues found.

Details:

  • repo_marker_key() scoping (lane-launcher.sh:436, mirrored in restart-consumer.sh). Adding -C "$REPO" to git hash-object only changes which repository's object format is used to digest an already-trusted, launcher-derived path string ($top/$REPO). No externally influenced data enters the digested value or the git invocation differently than before; this is a pure correctness fix to key derivation, not a new trust boundary.
  • Gate-arm option forwarding (lane-launcher.sh:597-676, arm_stop_gate/gate_option_from_settings). The v:-prefix disambiguation between "unset" and "explicit empty" is plain string handling in bash (${sentinel#v:}, ${marker#v:}) with no eval/source/word-splitting risk. Forwarded values reach bash "$script" "${args[@]}" as discrete array elements, so arbitrary content in lane_stop_gate_marker/lane_stop_gate_sentinel (spaces, quotes, newlines) cannot break argument boundaries or inject additional flags/commands.
  • GATE_ENTRY_REQUESTED jq-filter interpolation (lane-launcher.sh:585-676). This filter fragment is a fixed, single-quoted literal defined once in the script — never built from $1/$2 or any lane-supplied data — so its reuse across three jq programs via double-quote interpolation is not a jq-injection surface. The lane's settings JSON ($1/$settings) is always passed as jq's stdin (<<<), never spliced into the program text, and the option key ($2) is bound through --arg k rather than interpolated. This matches the pre-existing (unmodified) pattern in the file.
  • Net effect of the arm-id scoping fix is a hardening, not a weakening: previously every autonomy/autonomy@* entry (including ones that never requested the gate, and disabled ones) received the arm id and could have a non-requesting sibling's marker leak into the arm call; now only entries matching GATE_ENTRY_REQUESTED are touched. No new capability is granted to lower-trust input as a result of this change.
  • restart-consumer.sh's change is the identical one-line -C "$REPO" fix, same reasoning as above.
  • lane-launcher.test.sh changes are test fixtures only (including the new SHA-256 cross-format repo fixture, which uses $REAL_GIT directly rather than the PATH-stubbed git) — no production code path or trust boundary involved.
  • plugin.json (version bump) and CHANGELOG.md (prose) carry no executable logic or secrets.

Per instructions, this review defers to zizmor's advisory lane for supply-chain/unpinned-action risk, dangerous workflow triggers, excessive permissions, and template injection — none of the changed files are GitHub Actions workflows, so that lane is not applicable here anyway.

@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown

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


Claude's review

Summary

All three fixes are correct and the reasoning in the PR body checks out against the actual code.

#1 — marker key scoping. lane-launcher.sh:439 now does git -C "$REPO" hash-object --stdin, anchored on $REPO rather than the hashed $top, exactly as the PR describes — resolve_repo guarantees $REPO exists, while $top is a string git handed back and could be empty/unresolvable. restart-consumer.sh:458 mirrors it. I checked every other git hash-object call site in the plugin (grep across the tree): the README snippet and refresh.md's probe are both meant to run with cwd already inside the target checkout, so unscoped is correct there and they're rightly left untouched.

#2 — empty marker vs. absent. The v: sentinel-prefix trick in gate_option_from_settings (lane-launcher.sh:610-615) and its differentiated consumption in arm_stop_gate (lane-launcher.sh:674-676) is sound: [[ "$marker" == v:* ]] accepts an explicit empty value ("v:" itself matches), while [[ "$sentinel" == v:?* ]] requires at least one character after the prefix, correctly preserving the documented asymmetry (empty marker = "off"; empty sentinel = "not configured", since lane-stop-gate.sh substitutes a default token for it). I traced the jq string interpolation in all three call sites (lane_requests_stop_gate, gate_option_from_settings, arm_stop_gate's with_entries) and the escaping (\$k, \"..\") resolves to valid jq — no injection risk since $GATE_ENTRY_REQUESTED is a fixed literal and the only interpolated runtime value ($2/option key) is always a hardcoded caller-supplied string, never lane-controlled data.

#3 — arm-id scoping. GATE_ENTRY_REQUESTED is now the single predicate shared by detection, option extraction, and the with_entries injection (lane-launcher.sh:691-694), so a sibling autonomy@x entry that didn't request the gate can no longer receive lane_stop_gate_arm_id or leak its marker into the arm call. The second commit's revert of the multi-install fixture ordering (non-requesting entry now last) is necessary and correctly reasoned — with it first, "last match wins" would have passed even against the old unscoped code and pinned nothing.

Scope discipline: the middle commit's rollback (dropping the config_query_failed jq-error-swallowing fix and the SHA-256-fixture/asymmetric-sentinel bugs introduced in the first commit) is a good call — I confirmed no config_query_failed references remain anywhere in skills/lanes/scripts, so resolve_config is cleanly back to main's behavior with nothing orphaned.

Not independently verified: this review environment's Bash tool required interactive approval for every git/test invocation (including read-only ones like git fetch and running the test script), which wasn't available here, so I could not execute lane-launcher.test.sh myself. The "193 assertions, 0 failures" and "six assertions fail against the pre-fix launcher" claims in the PR body are author-claimed and unverified by me — my review is based on static reading of the diff and full file contents only.

No inline comments — I didn't find any defects to anchor to a line.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

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

…auncher-hardening

Renumber the claude-ops lane fixes to 0.27.5; main published 0.27.4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Code review complete

Summary

All three fixes are correct, and I traced each end-to-end rather than taking the PR body's reasoning at face value:

#1 — marker key scoping. -C "$REPO" on both git hash-object calls (lane-launcher.sh:439, restart-consumer.sh:458) anchors the digest to the target repo's object format instead of the caller's. resolve_repo() in both scripts (lane-launcher.sh:292-306, restart-consumer.sh:379-388) guarantees $REPO is an existing directory before either function can run, which is exactly the property the new comment claims for anchoring on $REPO rather than $top.

#2 — empty marker vs. absent. I traced the full round trip through the (unchanged, different-plugin) consumer: gate_option_from_settings prefixes matched values with v: (lane-launcher.sh:610-615); arm_stop_gate forwards --marker "" only when the prefixed value is v:* (matches empty) and --sentinel only when v:?* (requires ≥1 char after the prefix) (lane-launcher.sh:674-676). Downstream, plugins/autonomy/hooks/lane-stop-gate-arm.sh:126-137 writes {"marker": ""} into the arm record whenever --marker was passed at all (via its own MARKER_SET flag), and plugins/autonomy/hooks/lane-stop-gate.sh:196-215's gate_option() already applies the identical v:-prefix trick when reading the arm record back, so an explicit empty marker correctly resolves to MARKER=""[[ -f "" ]] false → channel off, while an empty sentinel is deliberately never recorded and the gate substitutes its own default (lane-stop-gate.sh:258-262, confirmed by reading that file). The asymmetry the PR describes is real and consistently implemented on both sides of a plugin boundary that this PR only touches half of.

#3 — arm-id scoping. GATE_ENTRY_REQUESTED is now the one predicate shared by lane_requests_stop_gate, gate_option_from_settings, and the with_entries injection in arm_stop_gate (lane-launcher.sh:124-180). The with_entries else . branch preserves non-matching entries untouched, so a disabled sibling's config survives structurally while never receiving the arm id or contributing its marker/sentinel — matches the new gate-multi.json fixture's assertions.

Test coverage. The new SHA-256 cross-format fixture correctly delegates to $REAL_GIT (not the PATH-stubbed git, which only understands pull/rev-parse/hash-object well enough to fake a plain directory as a repo) and is skipped gracefully where SHA-256 repo creation isn't available. The multi-install fixture's ordering note (non-requesting entry deliberately last, since "last match wins" would otherwise pass against the old code too) is correct and matches what I traced through gate_option_from_settings.

One documentation nit (posted inline at lane-launcher.sh:417): the comment block above repo_marker_key() still opens with "Two properties the key must have" (line 407) after this PR adds a third bullet ("Digested IN the target repository"). Purely cosmetic — worth a one-word fix to "Three".

Not independently verified: this environment's Bash tool requires interactive approval for git hash-object, git fetch, and running the test script, none of which was available here — same constraint the previous review run hit. I could not execute lane-launcher.test.sh myself, so the "193 assertions, 0 failures" and "six assertions fail against the pre-fix launcher" claims remain author-claimed and unverified by me. Everything above is from static reading of the full diff and the referenced (unchanged) autonomy plugin files, plus reasoning through the jq/bash logic by hand.

No blocking issues found.

Branch: fix/claude-ops-lane-launcher-hardening

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Security review complete

  • Gather context (PR diff vs origin/main, changed files)
  • Review lane-launcher.sh (marker-key scoping, gate-arm sentinel handling, arm-id scoping)
  • Review restart-consumer.sh (marker-key scoping)
  • Review lane-launcher.test.sh (test-only sanity check)
  • Review plugin.json / CHANGELOG.md (non-executable, sanity check)
  • Post final findings

Scope: all 5 files changed in this PR, diffed against origin/main. This is a re-run triggered by a synchronize event; the only change since the prior review (commit ccc93a9) was a merge of ~200 unrelated files from mainlane-launcher.sh, restart-consumer.sh, and lane-launcher.test.sh are byte-identical to what was already reviewed. I re-read all three in full rather than relying on that.

No security issues found.

  • Marker-key scoping (lane-launcher.sh#L433-L443, restart-consumer.sh mirror). Adding -C "$REPO" to git hash-object/git rev-parse only changes which repository's object format digests an already-trusted, launcher-derived path string ($top, falling back to $REPO itself — never externally supplied). No new input enters the digest and no shell metacharacter handling changed; this is key-derivation correctness, not a new trust boundary.
  • Gate-arm sentinel/marker forwarding (lane-launcher.sh#L610-L695). The v:-prefix disambiguation (gate_option_from_settings/arm_stop_gate) is plain bash string slicing (${sentinel#v:}, ${marker#v:}); forwarded values reach bash "$script" "${args[@]}" as discrete array elements (args+=(--sentinel "$val")), so no value — however it's spelled — can break argument boundaries or smuggle an extra flag.
  • GATE_ENTRY_REQUESTED jq interpolation (lane-launcher.sh#L594-L695). This filter fragment is a single fixed literal defined once (lane-launcher.sh#L594), never built from lane-supplied data, so splicing it into three jq programs via double-quote interpolation isn't a jq-injection surface. The lane's settings JSON is always jq's stdin (<<<), never spliced into program text; the only interpolated runtime value is the option-key parameter ($2), which is always a hardcoded caller string (lane_stop_gate_sentinel/lane_stop_gate_marker), never lane-controlled, and is bound safely via --arg k.
  • Net effect of the arm-id scoping fix is a hardening, not a weakening. Previously every autonomy/autonomy@* entry — including ones that never requested the gate, and explicitly disabled ones — received the arm id, and a non-requesting sibling's marker could leak into the arm call. Now only entries matching GATE_ENTRY_REQUESTED are touched, in detection, option extraction, and injection alike. No new capability is granted to lower-trust input.
  • --settings/prompt content never re-enters a shell. launch_lane (lane-launcher.sh#L780-L802) builds cmd as a bash array and invokes "${cmd[@]}" directly (no eval, no sh -c), so arbitrary bytes in a lane's settings JSON or prompt file content cannot be interpreted as additional arguments or commands.
  • lane-launcher.test.sh changes (new SHA-256 cross-format fixture + four gate-arming fixtures) are test-only; the SHA-256 fixture correctly uses $REAL_GIT directly rather than the PATH-stubbed git, so it exercises real object-format behavior rather than the stub.
  • plugin.json (version bump, plain em-dashes re-encoded as escapes) and CHANGELOG.md (prose) carry no executable logic or secrets.

This repo's zizmor advisory lane covers supply-chain/unpinned-action risk, dangerous workflow triggers, excessive permissions, and template injection; none of the changed files are GitHub Actions workflows, so that lane doesn't apply here.

Comment thread plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh
@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Security review complete

  • Gather context (gh pr diff against origin/main, read changed files)
  • Review lane-launcher.sh marker-key + gate-arming changes
  • Review restart-consumer.sh marker-key change
  • Review lane-launcher.test.sh (test-only, sanity check)
  • Review plugin.json / CHANGELOG.md (non-executable, sanity check)
  • Post findings as PR review

Scope: all 5 files changed in this PR, diffed against origin/main at HEAD 901542d. No files in .github/workflows are touched, so the zizmor advisory lane's coverage (supply-chain/unpinned-action risk, dangerous triggers, excessive permissions, template injection) doesn't apply here.

No security issues found.

Reasoning, independently traced against the diff and the surrounding (unchanged) code:

  • Marker-key scoping (lane-launcher.sh#L439, mirrored in restart-consumer.sh#L458). Adding -C "$REPO" to git hash-object only changes which repository's object format digests an already-trusted, launcher-derived string ($top, itself sourced from git -C "$REPO" rev-parse --show-toplevel or the $REPO fallback — never externally supplied). resolve_repo() guarantees $REPO is an existing directory before this runs. No new input enters the digest, no shell metacharacter handling changed. This is key-derivation correctness, not a new trust boundary.
  • Gate-arm sentinel/marker forwarding (lane-launcher.sh#L610-L695). The v: sentinel-prefix disambiguation (gate_option_from_settings / arm_stop_gate) is plain bash string slicing (${sentinel#v:}, ${marker#v:}). Forwarded values reach bash "$script" "${args[@]}" as discrete array elements (args+=(--marker "${marker#v:}")), so no value — however it's spelled, including embedded spaces/quotes/newlines — can break argument boundaries or smuggle an extra flag into the arm helper invocation.
  • GATE_ENTRY_REQUESTED jq-filter interpolation (lane-launcher.sh#L594-L695). This filter fragment is a single fixed literal defined once and never built from lane-supplied data, so splicing it into three jq programs via double-quote interpolation is not a jq-injection surface. The lane's settings JSON is always jq's stdin (<<<), never spliced into program text; the only interpolated runtime value is the option-key parameter ($2), always a hardcoded caller string (lane_stop_gate_sentinel / lane_stop_gate_marker) bound safely via --arg k, never lane-controlled.
  • Net effect of the arm-id scoping fix (fix(autonomy,lanes): read the lane-stop gate's enable flag from trusted channels only #1865) is a hardening, not a weakening. Previously every autonomy/autonomy@* entry — including ones that never requested the gate, and explicitly disabled ones — received the arm id, and a non-requesting sibling's marker could leak into the arm call. Now only entries matching GATE_ENTRY_REQUESTED are touched, consistently across detection, option extraction, and injection. No new capability is granted to lower-trust input; the gate still never treats this channel as a trusted verdict on its own (an id on a false entry doesn't override that false).
  • --settings/prompt content never re-enters a shell. launch_lane (lane-launcher.sh#L780-L802) builds cmd as a bash array and invokes "${cmd[@]}" directly — no eval, no sh -c — so arbitrary bytes in a lane's settings JSON or prompt file content can't be interpreted as additional arguments or commands. This code path is unchanged by this PR but sits directly downstream of the modified functions, so it was worth re-confirming.
  • restart-consumer.sh's change is the identical one-line -C "$REPO" fix, same reasoning as above.
  • lane-launcher.test.sh changes (new SHA-256 cross-format fixture + four gate-arming fixtures) are test-only, exercised via $REAL_GIT rather than the PATH-stubbed git, so they run real object-format behavior — no production code path or trust boundary involved.
  • plugin.json (version bump) and CHANGELOG.md (prose) carry no executable logic or secrets.

This is a synchronize-triggered re-review; I independently re-read the full diff and surrounding code rather than relying on the prior review comments on this PR, and reached the same conclusion each has reached: no security issues.

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Code review complete

Summary

This is a synchronize re-review at HEAD 901542d (merge of origin/main into the branch). The three production files (lane-launcher.sh, restart-consumer.sh, lane-launcher.test.sh) are unchanged since the prior review pass — I re-diffed against the branch's actual merge-base with origin/main (40f6533) and re-traced the logic rather than relying on that.

All three fixes are correct, well-reasoned, and match what the PR body claims:

  • chore: initialize marketplace scaffold #1 — marker key scoping. -C "$REPO" on git hash-object (lane-launcher.sh:439, restart-consumer.sh mirror) anchors both the digest and its rev-parse --show-toplevel input to the target repo, not the caller. resolve_repo() in both scripts guarantees $REPO is an existing directory before either function runs, which is exactly the property the new comment claims for anchoring on $REPO rather than $top. refresh.md's probe and the README snippet are correctly left untouched — both already run with cwd inside the target checkout.
  • fix: correct plugin-system inaccuracies and harden references #2 — empty marker vs. absent. Traced end-to-end: gate_option_from_settings prefixes matched values with v:; arm_stop_gate forwards --marker "" when the value is v:* (matches empty) and --sentinel only when v:?* (≥1 char after the prefix). The asymmetry is intentional and consistently applied — an empty marker is a real "off" signal, an empty sentinel is not a configured value (the gate substitutes its own default).
  • feat: markdown-formatter plugin (pilot migration) #3 — arm-id scoping. GATE_ENTRY_REQUESTED is now the one predicate shared by lane_requests_stop_gate, gate_option_from_settings, and the with_entries injection in arm_stop_gate — a non-requesting sibling entry can no longer receive the arm id or leak its marker into the arm call. arm_stop_gate is only ever invoked after lane_requests_stop_gate succeeds (lane-launcher.sh:759, 772), so the "requesting" predicate is never evaluated against a settings object with zero matches.
  • Outstanding nit still present: the doc comment block above repo_marker_key() still opens with "Two properties the key must have" (lane-launcher.sh#L407) even though a third bullet was added at L417-430. This was already flagged as an inline comment on this PR; it wasn't addressed in the commits since, so re-noting it here rather than re-posting the same inline thread.

New finding — test-coverage gap in restart-consumer.test.sh (not a file this PR touches, but directly adjacent to the fix it verifies). While confirming the README/refresh.md hand-recompute snippets were correctly left unscoped, I grepped the plugin for every hash-object call site and found three in restart-consumer.test.sh that pre-compute a marker key to seed fixture data (L209, L407, L588):

KEY="$(printf '%s' "$(git -C "$REPO" rev-parse --show-toplevel)" | git hash-object --stdin)"

The rev-parse half is scoped with -C "$REPO", but the hash-object half is not — the identical unscoped pattern this PR fixes in production code. It's harmless today only because the test runner's own git and $REPO share one object format (SHA-1) in this suite, so these three assertions would pass identically whether or not restart-consumer.sh's -C fix were applied — they don't pin the fix the way lane-launcher.test.sh's new SHA-256 cross-format fixture pins the launcher's identical fix. The PR body's "every fix is covered by a new assertion that fails against the pre-fix code" claim, read narrowly, is about lane-launcher.test.sh/the launcher — but restart-consumer.sh's mirrored fix currently has no equivalent regression test. Worth either adding a SHA-256 cross-format fixture here too (mirroring the one just added to lane-launcher.test.sh), or at minimum scoping these three key computations with -C "$REPO" on the hash-object call for consistency, ideally hoisted into a shared helper (lane-launcher.test.sh already extracted one: marker_repo_key()) instead of the same 3-line snippet repeated three times. Not blocking — the shipped fix itself is correct — but it's a real gap in what actually verifies it.

Test execution: this environment's Bash tool required interactive approval to run bash lane-launcher.test.sh (and I could not get it approved), so — consistent with every prior review pass on this PR — I could not execute the suite myself. The "193 assertions, 0 failures" and "six assertions fail against the pre-fix launcher" claims remain author-claimed and unverified by me; everything above comes from static reading of the full diff, the referenced files at HEAD, and tracing the jq/bash logic by hand.

No blocking issues. Security-relevant aspects are covered by the dedicated security lane.

The `-C` scoping added a third bullet to a list still introduced as two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Security review complete

  • Gather context (gh pr diff against origin/main, changed files)
  • Review lane-launcher.sh changes
  • Review restart-consumer.sh changes
  • Review lane-launcher.test.sh changes (test-only, sanity check)
  • Review plugin.json / CHANGELOG.md (non-security, sanity check)
  • Post findings

Scope: all 5 files changed in this PR, diffed against origin/main via gh pr diff 2050, at HEAD a9027a4. No files under .github/workflows are touched, so zizmor's advisory lane (supply-chain/unpinned-action risk, dangerous triggers, excessive permissions, template injection) doesn't apply here.

No security issues found.

I independently re-traced each change rather than relying on the prior review passes on this PR (all of which also found nothing):

  • Marker-key scoping (lane-launcher.sh#L439, mirrored in restart-consumer.sh#L458). Adding -C "$REPO" to git hash-object only changes which repository's object format digests an already-trusted, launcher-derived string ($top, itself from git -C "$REPO" rev-parse --show-toplevel, falling back to $REPO). I confirmed resolve_repo() in both scripts (lane-launcher.sh#L291, restart-consumer.sh#L379) guarantees $REPO is an existing directory before either function can run, so -C "$REPO" cannot be steered at an attacker-controlled path. No new input enters the digest and no shell metacharacter handling changed — pure key-derivation correctness, not a new trust boundary.
  • Gate-arm sentinel/marker forwarding (lane-launcher.sh#L610-L695). The v: sentinel-prefix disambiguation (gate_option_from_settings/arm_stop_gate) is plain bash string slicing (${sentinel#v:}, ${marker#v:}). Forwarded values reach bash "$script" "${args[@]}" as discrete array elements, so no value — however it's spelled, including embedded spaces/quotes/newlines/$(...) — can break argument boundaries or smuggle an extra flag into the arm helper invocation.
  • GATE_ENTRY_REQUESTED jq-filter interpolation (lane-launcher.sh#L594-L695). Verified this fragment is a single fixed literal defined once and never built from lane-supplied data, so splicing it into three jq programs via double-quote interpolation isn't a jq-injection surface. The lane's settings JSON is always jq's stdin (<<<), never spliced into program text; the only interpolated runtime value is the option-key parameter ($2), always a hardcoded caller string (lane_stop_gate_sentinel/lane_stop_gate_marker, never lane-controlled), bound safely via --arg k.
  • Net effect of the arm-id scoping fix (fix(autonomy,lanes): read the lane-stop gate's enable flag from trusted channels only #1865) is a hardening, not a weakening. Previously every autonomy/autonomy@* entry — including ones that never requested the gate, and explicitly disabled ones — received the arm id, and a non-requesting sibling's marker could leak into the arm call. Now only entries matching GATE_ENTRY_REQUESTED are touched, consistently across detection, option extraction, and injection. No new capability is granted to lower-trust input; the gate still never treats this channel as a trusted verdict on its own.
  • Settings/prompt content never re-enters a shell. launch_lane (lane-launcher.sh#L764-L800) builds cmd as a bash array and invokes "${cmd[@]}" directly — no eval, no sh -c — so arbitrary bytes in a lane's settings JSON or prompt file content can't be interpreted as additional arguments or commands. This path is unchanged by the PR but sits directly downstream of the modified functions, so I re-confirmed it holds.
  • lane-launcher.test.sh changes (new SHA-256 cross-format fixture + four gate-arming fixtures) are test-only, exercised via $REAL_GIT rather than the PATH-stubbed git — no production code path or trust boundary involved.
  • plugin.json (version bump) and CHANGELOG.md (prose) carry no executable logic or secrets.

This is the fifth independent review pass on this PR (across security and code-review lanes); each has reached the same conclusion via independent tracing. No blocking security issues.
· Branch: fix/claude-ops-lane-launcher-hardening

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Code review complete

  • Gather context (diff since last review, changed files)
  • Check whether the previously-flagged "Two properties" nit was addressed
  • Review remaining diff for correctness/maintainability
  • Post findings

Summary

This is a synchronize re-review. Since the last full review pass (HEAD 901542d), exactly one commit was added — a9027a4, a one-line docs-only change:

-# Two properties the key must have, both learned the hard way:
+# Three properties the key must have, all learned the hard way:

I confirmed this is the only change (git diff 901542d..a9027a4 --stat touches one file, one line) and that it resolves the inline nit from the prior review pass — the comment block above repo_marker_key() in lane-launcher.sh now correctly says "Three properties," matching the three bullets that follow it (injective, digested in the target repo, and the earlier-established stability property).

I also re-diffed the full PR against the merge-base (40f6533, the commit 901542d's merge pulled from origin/main) scoped to plugins/claude-ops/: 5 files changed, +223/-24, matching the PR's reported stat exactly — confirming no other production logic changed since the prior full review.

Everything substantive from the prior review passes still holds (marker-key scoping fix in lane-launcher.sh/restart-consumer.sh, the v:-sentinel empty-marker-vs-absent fix, and the GATE_ENTRY_REQUESTED arm-id scoping fix) — all three are correct, traced end-to-end, and covered by the new test fixtures described in the PR body. I have nothing new to add there.

Outstanding from a previous pass, not part of this PR's file set: the earlier-noted test-coverage gap in restart-consumer.test.sh (three hash-object calls at lines ~209/407/588 that pre-compute a marker key without -C "$REPO" on the hash-object half, mirroring the bug this PR fixes in production code) is unchanged — that file isn't among this PR's 5 changed files, so it's unaffected by this synchronize. Not blocking; noting for continuity in case a follow-up is wanted.

Test execution: as with every prior pass on this PR, this environment's Bash tool required interactive approval for bash lane-launcher.test.sh, which wasn't available here, so I could not execute the suite myself. The "193 assertions, 0 failures" claim remains author-claimed and unverified by me.

No blocking issues. No new inline comments — the one from the prior pass is now resolved by a9027a4.
· Branch: fix/claude-ops-lane-launcher-hardening

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Security review complete

  • Gather context (git diff origin/main...HEAD, changed files)
  • Review lane-launcher.sh changes
  • Review restart-consumer.sh changes
  • Review lane-launcher.test.sh changes (test-only, sanity check)
  • Review plugin.json / CHANGELOG.md (non-security, sanity check)
  • Post findings

Scope: all 5 files changed in this PR, diffed against origin/main at HEAD cc7f009. No files under .github/workflows are touched, so zizmor's advisory lane (supply-chain/unpinned-action risk, dangerous triggers, excessive permissions, template injection) doesn't apply here.

No security issues found.

This is a synchronize re-review. I confirmed git diff a9027a4..cc7f009 touches none of this PR's 5 files — the merge only pulled in one unrelated upstream commit (17ab4f5, a docs change to a different plugin) — so lane-launcher.sh, restart-consumer.sh, and lane-launcher.test.sh are byte-identical to what was already reviewed. I re-read the full diff independently rather than relying on that:

  • Marker-key scoping (lane-launcher.sh#L439, mirrored in restart-consumer.sh#L458). Adding -C "$REPO" to git hash-object only changes which repository's object format digests an already-trusted, launcher-derived string ($top, itself from git -C "$REPO" rev-parse --show-toplevel, falling back to $REPO). resolve_repo() guarantees $REPO is an existing directory before either function runs, so -C cannot be steered at an attacker-controlled or nonexistent path. No new input enters the digest and no shell metacharacter handling changed — key-derivation correctness, not a new trust boundary.
  • Gate-arm sentinel/marker forwarding (lane-launcher.sh#L610-L695). The v: sentinel-prefix disambiguation (gate_option_from_settings/arm_stop_gate) is plain bash string slicing (${sentinel#v:}, ${marker#v:}). Forwarded values reach bash "$script" "${args[@]}" as discrete array elements, so no value — however it's spelled, including embedded spaces/quotes/newlines/$(...) — can break argument boundaries or smuggle an extra flag.
  • GATE_ENTRY_REQUESTED jq-filter interpolation (lane-launcher.sh#L594-L695). Confirmed this fragment is a single fixed literal defined once, never built from lane-supplied data, so splicing it into three jq programs via double-quote interpolation is not a jq-injection surface. The lane's settings JSON is always jq's stdin (<<<), never spliced into program text; the only interpolated runtime value is the option-key parameter ($2), always a hardcoded caller string (lane_stop_gate_sentinel/lane_stop_gate_marker, never lane-controlled), bound safely via --arg k.
  • Net effect of the arm-id scoping fix (fix(autonomy,lanes): read the lane-stop gate's enable flag from trusted channels only #1865) is a hardening, not a weakening. Previously every autonomy/autonomy@* entry — including ones that never requested the gate, and explicitly disabled ones — received the arm id, and a non-requesting sibling's marker could leak into the arm call. Now only entries matching GATE_ENTRY_REQUESTED are touched, consistently across detection, option extraction, and injection. No new capability is granted to lower-trust input; the gate still never treats this channel as a trusted verdict on its own.
  • Settings/prompt content never re-enters a shell. launch_lane builds cmd as a bash array and invokes "${cmd[@]}" directly — no eval, no sh -c — so arbitrary bytes in a lane's settings JSON or prompt file content can't be interpreted as additional arguments or commands. Unchanged by this PR but directly downstream of the modified functions; re-confirmed it still holds.
  • lane-launcher.test.sh changes (SHA-256 cross-format fixture + four gate-arming fixtures) are test-only, exercised via $REAL_GIT rather than the PATH-stubbed git — no production code path or trust boundary involved.
  • plugin.json (version bump) and CHANGELOG.md (prose) carry no executable logic or secrets.

This is the seventh independent review pass on this PR (across security and code-review lanes), all reaching the same conclusion. No blocking security issues.
· Branch: fix/claude-ops-lane-launcher-hardening

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Code review complete

  • Gather context (diff since last review, changed files)
  • Check what changed since the last code-review pass (HEAD a9027a4cc7f009)
  • Confirm no new production-code changes in scope
  • Post findings

Summary

This is a synchronize re-review. I confirmed the diff between the previously-reviewed HEAD (a9027a4) and the current HEAD (cc7f009) touches zero files under plugins/claude-ops/:

git diff a9027a4..cc7f009 --stat -- plugins/claude-ops/
(no output)

The two new commits on the branch are a merge of origin/main (17ab4f5, docs for rate-limit-guard) plus the merge commit itself — 33 files changed, none in plugins/claude-ops/ (context-guard, discovery, rate-limit-guard, source-control, work-items plugins and prompts/loops/, all unrelated to this PR).

I also re-confirmed the full PR diff against origin/main is still exactly the 5 files / +223/-24 the PR reports:

git diff origin/main...HEAD --stat -- plugins/claude-ops/
 plugins/claude-ops/.claude-plugin/plugin.json      |   2 +-
 plugins/claude-ops/CHANGELOG.md                    |  40 +++++++
 .../skills/lanes/scripts/lane-launcher.sh          |  79 ++++++++++----
 .../skills/lanes/scripts/lane-launcher.test.sh     | 119 ++++++++++++++++++++-
 .../skills/lanes/scripts/restart-consumer.sh       |   7 +-
 5 files changed, 223 insertions(+), 24 deletions(-)

Nothing new to review. All findings from the prior full code-review pass (31297111954) still hold:

Test execution: as with every prior pass on this PR, this environment's Bash tool requires interactive approval to run bash lane-launcher.test.sh, which wasn't available here, so I could not execute the suite myself. The "193 assertions, 0 failures" claim remains author-claimed and unverified by me.

No blocking issues. No new inline comments — this synchronize introduced no changes in scope for this PR.
· Branch: fix/claude-ops-lane-launcher-hardening

…auncher-hardening

Renumber the lane fixes to 0.27.6; main published 0.27.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Security review complete

  • Gather context (gh pr diff against origin/main, changed files)
  • Check what changed since the last reviewed HEAD (cc7f009)
  • Review lane-launcher.sh changes
  • Review restart-consumer.sh changes
  • Review lane-launcher.test.sh changes (test-only, sanity check)
  • Review plugin.json / CHANGELOG.md (non-security, sanity check)
  • Post findings

Scope: all 5 files changed in this PR, diffed against origin/main at HEAD ac95766. No files under .github/workflows are touched, so zizmor's advisory lane (supply-chain/unpinned-action risk, dangerous triggers, excessive permissions, template injection) doesn't apply here.

No security issues found.

This is another synchronize re-review. I diffed cc7f009 (the HEAD of the prior full review pass) against the current HEAD (ac95766), scoped to this PR's 5 files: lane-launcher.sh, restart-consumer.sh, and lane-launcher.test.sh are byte-identical — the only changes are a version bump in plugin.json (0.27.50.27.6) and a new CHANGELOG.md entry documenting an unrelated README wiring fix (hook-utils.sh dependency), neither of which touches executable logic. I independently re-read the full diffs of lane-launcher.sh and restart-consumer.sh against origin/main rather than relying on that stat alone:

  • Marker-key scoping (lane-launcher.sh#L439, mirrored in restart-consumer.sh#L458). -C "$REPO" on git hash-object only changes which repository's object format digests an already-trusted, launcher-derived string ($top, itself from git -C "$REPO" rev-parse --show-toplevel, falling back to $REPO). resolve_repo() guarantees $REPO is an existing directory before either function runs, so the -C target can't be steered to an attacker-controlled or nonexistent path. No new input enters the digest and no shell metacharacter handling changed — key-derivation correctness, not a new trust boundary.
  • Gate-arm sentinel/marker forwarding (lane-launcher.sh#L610-L695). The v: sentinel-prefix disambiguation (gate_option_from_settings/arm_stop_gate) is plain bash string slicing (${sentinel#v:}, ${marker#v:}). Forwarded values reach bash "$script" "${args[@]}" as discrete array elements, so no value — however it's spelled, including embedded spaces/quotes/newlines/$(...) — can break argument boundaries or smuggle an extra flag.
  • GATE_ENTRY_REQUESTED jq-filter interpolation (lane-launcher.sh#L594-L695). Confirmed this fragment is a single fixed literal defined once, never built from lane-supplied data, so splicing it into three jq programs via double-quote interpolation is not a jq-injection surface. The lane's settings JSON is always jq's stdin (<<<), never spliced into program text; the only interpolated runtime value is the option-key parameter ($2), always a hardcoded caller string, bound safely via --arg k, never lane-controlled.
  • Net effect of the arm-id scoping fix (fix(autonomy,lanes): read the lane-stop gate's enable flag from trusted channels only #1865) is a hardening, not a weakening. Previously every autonomy/autonomy@* entry — including ones that never requested the gate, and explicitly disabled ones — received the arm id, and a non-requesting sibling's marker could leak into the arm call. Now only entries matching GATE_ENTRY_REQUESTED are touched, consistently across detection, option extraction, and injection. No new capability is granted to lower-trust input; the gate still never treats this channel as a trusted verdict on its own.
  • lane-launcher.test.sh changes (SHA-256 cross-format fixture + four gate-arming fixtures) are test-only, exercised via $REAL_GIT rather than the PATH-stubbed git — no production code path or trust boundary involved.
  • plugin.json (version bump) and CHANGELOG.md (prose, documents an unrelated README fix) carry no executable logic or secrets.

This is now the eighth independent review pass on this PR (across security and code-review lanes), all reaching the same conclusion via independent tracing. No blocking security issues.
· Branch: fix/claude-ops-lane-launcher-hardening

@claude

claude Bot commented Aug 9, 2026

Copy link
Copy Markdown

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


Security review complete

  • Gather context (git diff origin/main...HEAD, changed files)
  • Check what changed since the last reviewed HEAD (ac95766)
  • Review lane-launcher.sh changes
  • Review restart-consumer.sh changes
  • Review lane-launcher.test.sh changes (test-only, sanity check)
  • Review plugin.json / CHANGELOG.md (non-security, sanity check)
  • Post findings

Scope: all 5 files changed in this PR, diffed against origin/main at HEAD 482102c. No files under .github/workflows are touched, so zizmor's advisory lane (supply-chain/unpinned-action risk, dangerous triggers, excessive permissions, template injection) doesn't apply here.

No security issues found.

This is another synchronize re-review. git diff ac95766..482102c -- plugins/claude-ops/ is empty — the merge that produced this HEAD pulled in unrelated changes to other plugins (autonomy, claude-config, guardrails, miro) and a new .github/workflows/claude-security-review.yml, none of which touch this PR's 5 files. lane-launcher.sh, restart-consumer.sh, and lane-launcher.test.sh are byte-identical to the previously reviewed HEAD.

One of the merged-in files is worth calling out even though it's outside this PR's diff: plugins/autonomy/hooks/lane-stop-gate.sh picked up a claim-sidecar mechanism (gate_arm_owned) to close a first-presenter race on gate_load_arm_record. I checked whether that change alters the interaction this PR's fix #2/#3 depend on — it doesn't. The claim sidecar only changes who owns an already-loaded arm record; it doesn't touch --marker/--sentinel flag parsing or MARKER_SET handling in lane-stop-gate-arm.sh, so the v:-prefix empty-vs-absent distinction this PR establishes on the lane-launcher.sh side still lands correctly on the consumer side. Not a finding — just confirming this PR's fix wasn't silently invalidated by an unrelated concurrent change.

Re-confirming the reasoning independently against the unchanged diff:

  • Marker-key scoping (lane-launcher.sh#L439, mirrored in restart-consumer.sh#L458). -C "$REPO" on git hash-object only changes which repository's object format digests an already-trusted, launcher-derived string ($top, itself from git -C "$REPO" rev-parse --show-toplevel, falling back to $REPO). resolve_repo() guarantees $REPO is an existing directory before either function runs, so the -C target can't be steered to an attacker-controlled or nonexistent path. No new input enters the digest — key-derivation correctness, not a new trust boundary.
  • Gate-arm sentinel/marker forwarding (lane-launcher.sh#L610-L695). The v: sentinel-prefix disambiguation (gate_option_from_settings/arm_stop_gate) is plain bash string slicing (${sentinel#v:}, ${marker#v:}). Forwarded values reach bash "$script" "${args[@]}" as discrete array elements, so no value — however it's spelled, including embedded spaces/quotes/newlines/$(...) — can break argument boundaries or smuggle an extra flag.
  • GATE_ENTRY_REQUESTED jq-filter interpolation (lane-launcher.sh#L594-L695). Confirmed this fragment is a single fixed literal defined once, never built from lane-supplied data, so splicing it into three jq programs via double-quote interpolation is not a jq-injection surface. The lane's settings JSON is always jq's stdin (<<<), never spliced into program text; the only interpolated runtime value is the option-key parameter ($2), always a hardcoded caller string, bound safely via --arg k, never lane-controlled.
  • Net effect of the arm-id scoping fix (fix(autonomy,lanes): read the lane-stop gate's enable flag from trusted channels only #1865) is a hardening, not a weakening. Previously every autonomy/autonomy@* entry — including ones that never requested the gate, and explicitly disabled ones — received the arm id, and a non-requesting sibling's marker could leak into the arm call. Now only entries matching GATE_ENTRY_REQUESTED are touched, consistently across detection, option extraction, and injection. No new capability is granted to lower-trust input; the gate still never treats this channel as a trusted verdict on its own.
  • lane-launcher.test.sh changes (SHA-256 cross-format fixture + four gate-arming fixtures) are test-only, exercised via $REAL_GIT rather than the PATH-stubbed git — no production code path or trust boundary involved.
  • plugin.json (version bump) and CHANGELOG.md (prose) carry no executable logic or secrets.

This is now the ninth independent review pass on this PR (across security and code-review lanes), all reaching the same conclusion via independent tracing, and the underlying diff has not changed since the eighth pass. No blocking security issues.
· Branch: fix/claude-ops-lane-launcher-hardening

@kyle-sexton
kyle-sexton merged commit 1f15491 into main Aug 9, 2026
33 checks passed
@kyle-sexton
kyle-sexton deleted the fix/claude-ops-lane-launcher-hardening branch August 9, 2026 08:58
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.

1 participant