feat(context-guard): version-independent statusline shim for both guard plugins - #1252
Conversation
…rd plugins
Replaces version-pinned statusline wiring with a shim the operator wires
once. ${CLAUDE_PLUGIN_ROOT} is version-pinned and the old version directory
is pruned ~14 days after an update (plugins reference, fetched 2026-07-24),
so a statusLine wired to <plugin-root>/scripts/statusline-tee.sh silently
stops teeing at the next version bump and then takes the operator's WHOLE
statusline down once the path disappears (bash <missing> -> 127).
scripts/statusline-shim.sh (both plugins) resolves the newest installed tee
at run time — newest by mtime across marketplaces, skipping transient temp_*
cache clones — and is transparent in every path: no tee installed degrades
to running the wrapped statusline alone; a wired-standalone shim prints one
diagnostic line rather than leaving a blank bar. Pure Bash builtins, so it
adds no subprocess to the statusline path.
setup apply installs it byte-identically to ~/.claude/<plugin>/bin/, which
widens context-guard's apply scope (previously zones.json only) and gives
rate-limit-guard an apply action for the first time. The copy is inert until
the operator applies the printed settings.json edit, so the context-guard
review record's no-kill-switch justification survives; recorded as a delta
security review in MIGRATION-PLAYBOOK.md. setup check gained an installed-
shim state check and now classifies cache-path wiring as LEGACY regardless
of whether that file still exists.
Both setup skills print the sibling-composition wiring (each tee through its
own shim, innermost command owning stdout) and state the measured per-tee
cost: ~0.6-0.9 s per refresh on Windows/Git Bash, process-spawn bound.
Resolves the plugin-audit-port PLAN's stable-shim open question (stress-test
#4) in favor of the shim; the interim [ -f ] guard and an inline settings
glob were both weighed and rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 48s —— View job PR Review: feat(context-guard): version-independent statusline shim for both guard pluginsStatus: Review complete. Tasks:
SummaryThe problem statement is correct and well-understood: Findings1. Settings-scope coverage asymmetry between the two SKILL.md files (medium)
The local settings file is not mentioned, and "note any project-level shadow" is weaker than "print the edit targeting the shadowing file". An operator whose statusline is wired at project or local scope would get LEGACY detection applied to the wrong file (and the printed edit would target the wrong file too). The asymmetry isn't obviously intentional — rate-limit-guard is not architecturally simpler in a way that would justify reading fewer scopes. 2. Comment drift between the two shims on line 25 (low)
"The tee file" sounds like the script file itself disappears; "the snapshot" (context-guard) or "the contract file" (matching the wording used elsewhere in rate-limit-guard docs) would be clearer. The two shims are intended to stay in sync — this comment divergence shows how drift accumulates without a drift gate on the script files themselves. 3. No CI gate enforcing shim-to-shim behavioral equivalence (low)The two shim scripts are identical modulo 4.
|
|
Claude finished @kyle-sexton's task in 3m 44s —— View job Security Review
FindingsScope: SUGGESTION — mtime-based tee resolution has no integrity bound, creating a durable malware persistence vectorSeverity: SUGGESTION · Confidence: PLAUSIBLE (requires prior same-user compromise) context-guard shim, lines 57–66 / rate-limit-guard shim, lines 57–66 for cand in "$cache"/*/"$PLUGIN_NAME"/*/scripts/statusline-tee.sh; do
[[ -f "$cand" ]] || continue
rest="${cand#"$cache"/}"; mkt="${rest%%/*}"
[[ "$mkt" == temp_* ]] && continue
if [[ -z "$RESOLVED" || "$cand" -nt "$RESOLVED" ]]; then
RESOLVED="$cand"
fi
doneThe shim resolves the tee by selecting the newest-by-mtime entry under The delta security review in MIGRATION-PLAYBOOK.md states the shim "has no untrusted input (its only inputs are its own argv and the cache directory listing)." The cache directory listing is an untrusted input from a defense-in-depth perspective: it's under The privilege concern is bounded: this does not escalate privilege (the planting process must already run as this user). What it does provide is a stable, self-reinvoking execution vector on every statusline refresh that survives process exits. An attacker who can write to Mitigation options to consider: hash the resolved path against the expected tee's checksum before exec'ing, or restrict the glob to a single known-good marketplace name (acknowledged as a design trade-off in the PR). SUGGESTION —
|
…ord resolution evidence check step 2 compared the installed shim against the shipped source with no state for "the shipped source does not exist" — which is every consumer still on 0.1.0, including this machine at authoring time. Both setup skills now report that branch as INFO (installed plugin predates the shim; /plugin update then re-run) and explicitly never call the operator's copy drifted there. Both shims record the empirical basis for newest-by-mtime resolution: the cache copy does NOT preserve source timestamps (a source committed 03:08 installed at 12:38 carried 12:38), so newest mtime is most-recently-installed even against an orphaned older version directory. Notes the authoritative alternative (~/.claude/plugins/installed_plugins.json) and why it is not used: undocumented internal schema, and it would put a jq spawn on every statusline refresh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 10s —— View job PR Review: feat(context-guard): version-independent statusline shim for both guard pluginsStatus: Review complete. Tasks:
ContextThis is a second-pass review. Two prior Claude reviews already landed (commit b34e5d4 review + security review). The latest push (commit 3c24a5d) adds:
Findings1. Scope asymmetry in rate-limit-guard step 3 is still unaddressed (medium — carry-over from prior review)context-guard SKILL.md L58–65:
rate-limit-guard SKILL.md L57–58:
The local settings file ( 2. New commit: pre-shim version-skew state is correct (no action needed)context-guard SKILL.md L53–57 / rate-limit-guard L52–56 The new "SHIPPED source is absent" branch correctly handles the transition case: if the operator is running plugin version < 0.2.0, 3. New commit: empirical mtime note strengthens the design (no action needed)context-guard shim L37–45 / rate-limit-guard L38–46 The observation that the cache copy carries install time (not source commit time) is the key premise of the mtime-based resolution algorithm. Recording the empirical basis here is the right call: it documents why newest-by-mtime equals most-recently-installed even against an orphaned older version directory, and it explains why the 4. Comment drift on shim line 25 still present (low — carry-over)context-guard shim L24: "The tee file" sounds like the script file disappears. "The contract file" or "the tee output" would match rate-limit-guard's elsewhere-used terminology and would parallel context-guard's "the snapshot" clearly. Not addressed in the new commit. What's done well in this pushThe pre-shim-absent logic is a meaningful gap filled: without it, VerdictFinding #1 (scope asymmetry in rate-limit-guard step 3) is the one outstanding issue with real behavior impact and was not addressed in this push. Findings #4 is low-severity. Findings #2 and #3 confirm the new commit's changes are correct. The overall implementation — shim resolution, transparency, test coverage, security delta review — remains sound. |
|
Claude finished @kyle-sexton's task in 4m 16s —— View job Security Review — PR #1252Tasks:
Scope: FindingsIMPORTANT — Open-glob mtime-plant is an undisclosed persistent execution vector · Confidence: PLAUSIBLELocation: for cand in "$cache"/*/"$PLUGIN_NAME"/*/scripts/statusline-tee.sh; do
[[ -f "$cand" ]] || continue
rest="${cand#"$cache"/}"; mkt="${rest%%/*}"
[[ "$mkt" == temp_* ]] && continue
if [[ -z "$RESOLVED" || "$cand" -nt "$RESOLVED" ]]; then
RESOLVED="$cand"
fi
done
...
exec bash "$RESOLVED" "$@"The This is a new attack surface introduced by this PR. The prior version-pinned wiring only executed one specific fixed path; the new glob-based resolution executes whatever matches the pattern, making the attack surface as wide as the operator's home directory. The MIGRATION-PLAYBOOK.md delta review states "the shim itself has no untrusted input (its only inputs are its own argv and the cache directory listing)" — but the cache directory listing is writable by same-user processes, and the review doesn't name the mtime-plant scenario as an acknowledged residual risk. Precondition: attacker already has same-user write access to Mitigation paths discussed in the prior review but not addressed in the delta record: checksum the resolved path against the bundled tee's hash before exec'ing; or restrict the glob to a documented known-good marketplace name (breaking repo-agnosticism but bounding the glob). SUGGESTION —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c24a5d5c1
ℹ️ 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".
…ng form Codex review (#1252, P2): the sibling-composition template substituted <current statusline command> raw, so an operator whose statusline carries shell syntax (THEME=dark my-statusline, a pipe, &&, ;) got THEME=dark as the wrapped EXECUTABLE. Verified: raw substitution through the chain fails "THEME=dark: command not found" (127); sh -c 'THEME=dark my-statusline' through the same chain renders and exits 0. Both setup skills now state that the innermost ARGV substitution rule is unchanged by nesting — only the shim paths nest — and print the sh -c variant of the combined form. rate-limit-guard's setup skill had no shell-syntax guard at all (it predates the context-guard one); it now carries the full guard plus the escaping rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 0s —— View job PR Review: feat(context-guard): version-independent statusline shim for both guard pluginsStatus: Review complete. Tasks:
Latest commit (6b1c35f): shell-syntax guard for combined wiring formThe Codex P2 finding is correctly addressed. Both plugins' setup skills now consistently handle shell-syntax commands across all wiring forms. What changed in this commit:
The commit message states it was verified against the failure case: One structural observation (no action needed): Both combined-wiring sections still show the plain Carry-over findings (not addressed in this commit)1. Scope asymmetry in rate-limit-guard step 3 — still present (medium)
The local settings file ( 2. Comment drift on shim line 24 — still present (low)
"The tee file" sounds like the script file itself is missing; "the contract file" or "the tee output" matches rate-limit-guard's elsewhere-used terminology and parallels context-guard's "the snapshot." Unchanged from the prior two reviews. VerdictThe latest commit is correct and clean — the Codex P2 finding (shell-syntax in combined wiring) is properly fixed in both plugins. Finding #1 (scope asymmetry in rate-limit-guard step 3) remains the only issue with real operator-impact. Finding #2 is low-severity polish. The core implementation — shim resolution, transparency, test coverage, security reasoning — continues to hold. |
|
Claude finished @kyle-sexton's task in 3m 25s —— View job Security Review — PR #1252Tasks:
Scope: The three commits on this branch add the shim scripts, fix the pre-shim version-skew state, and (latest, 6b1c35f) apply the shell-syntax guard to the combined wiring form. Each is covered below. IMPORTANT — Open-glob mtime-plant vector not named as acknowledged residual risk in the delta record · Confidence: PLAUSIBLE (same-user compromise required)Location: The resolution loop's only guard on marketplace directory names is the for cand in "$cache"/*/"$PLUGIN_NAME"/*/scripts/statusline-tee.sh; do
[[ -f "$cand" ]] || continue
rest="${cand#"$cache"/}"; mkt="${rest%%/*}"
[[ "$mkt" == temp_* ]] && continue
if [[ -z "$RESOLVED" || "$cand" -nt "$RESOLVED" ]]; then
RESOLVED="$cand"
fi
done
exec bash "$RESOLVED" "$@"
The shim then executes the planted file on every subsequent statusline refresh. The statusline continues rendering normally ( The MIGRATION-PLAYBOOK.md delta review states: "the shim itself has no untrusted input (its only inputs are its own argv and the cache directory listing)" — but the cache directory listing is writable by same-user processes and is therefore not free of untrusted input from a defense-in-depth standpoint. This specific scenario is not named as an acknowledged residual risk in the record. Precondition: attacker already has same-user write access to Recommendation: add a one-sentence acknowledgement in the delta review that the mtime-plant vector is a known, accepted residual risk contingent on prior same-user write-access compromise, and note which mitigation was rejected and why (consistent with how the SUGGESTION —
|
Per-finding classification — all reviewer findings on this PRValidated against head
Why the security-record findings (#5 / #9 / #10 / #13 / #14) are deferred, not fixed hereThey are all one root cause and the recommended remedy is to write an acceptance statement into
together with why the two named mitigations (hash the resolved path before Worth weighing when signing: the stated precondition (same-user write access to Fixing now#1/#7/#11 (settings-scope asymmetry) and #2/#8/#12 (comment drift) — both branch-owned, both verified still present at |
check step 3 read only the user's ~/.claude/settings.json and merely noted a project-level shadow, so an operator whose statusLine lives at project or local scope got LEGACY detection applied to the wrong file and the step 6 edit printed for the wrong file — it would apply cleanly and never run. context-guard's step 3 already scanned all three scopes and targeted the effective one; this closes the asymmetry with the same wording. The shim's no-tee contract line called the lost artifact "the tee file", which reads as the tee SCRIPT being gone rather than the snapshot it writes; the same header already says "contract directory" three lines down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 23s —— View job PR Review: fix(rate-limit-guard): scan every settings scope in setup check step 3Status: Review complete. Tasks:
What this commit fixesThis commit closes the two outstanding carry-over findings that survived three prior review cycles. Both changes are surgical — only two files touched, each targeting the exact identified gap. Finding #1/#7/#11 — Settings-scope asymmetry in
|
| Finding | Description | Status |
|---|---|---|
| #1/#7/#11 | Scope asymmetry in rate-limit-guard step 3 |
Fixed in this commit |
| #2/#8/#12 | Comment drift tee file → contract file on shim L24 |
Fixed in this commit |
| #16 | Shell-syntax guard missing from combined wiring form | Fixed in 6b1c35f |
| #3 | No CI gate for shim-to-shim behavioral equivalence | Deferred (own issue; beyond branch scope) |
| #4 | Redundant exit 0 after final printf |
Deferred (cosmetic; explicit exit documents terminal branch) |
| #5/#9/#13 | mtime-plant vector not named as accepted residual risk in delta record | Deferred (maintainer security-posture call, not automated) |
| #6/#15 | check cannot distinguish version-lag from tampering |
Deferred (design change; needs own issue) |
| #10/#14 | exec bash bypasses execute-bit; rides with delta-record amendment |
Deferred |
Verdict
All branch-owned findings are resolved. The implementation — shim resolution algorithm, transparency, test coverage (25 assertions per plugin), security delta review, changelog discipline, and wiring guidance — is correct and complete. The three deferred items are documented, intentional, and appropriately scoped to follow-up work rather than this branch.
Ready to merge.
|
Claude finished @kyle-sexton's task in 2m 11s —— View job Security Review — PR #1252 (commit 46c19e3)Tasks:
Scope: Latest commit (46c19e3) — no new security surfaces introducedThe commit fixes settings-scope asymmetry in
Both changes are clean. IMPORTANT — mtime-plant vector still absent from the delta security record · Confidence: PLAUSIBLE (same-user compromise required)Location: The delta review record still reads:
The claim "no untrusted input" is inaccurate from a defense-in-depth standpoint: This finding has been raised in every review cycle (four times, across two security reviews) and kyle-sexton's classification table explicitly defers it as a security-posture decision for the maintainers. The MIGRATION-PLAYBOOK.md was not touched in this commit. The recommended remedy — a one-sentence acceptance statement naming the vector as a known residual risk, with the rationale for why neither named mitigation was applied — remains unimplemented. The two mitigations discussed across prior reviews:
Neither rejection is currently recorded in the delta review, even though the SUGGESTION —
|
|
Follow-up to the classification above — the two
Verified before pushing: The remaining findings stay open by classification, not by omission: the security-record amendment (#5 / #9 / #10 / #13 / #14) needs a maintainer to sign the accepted-residual-risk statement, and the two design changes (#3 CI equivalence gate, #6 / #15 hashed |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46c19e3f7a
ℹ️ 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".
#1274) Closes #1268 ## Summary `docs/topics/plugin-audit-port/` rejected `context: fork` for the deep-audit phase on the stated grounds that a fork "inherits the degraded history the gate exists to escape" (`PLAN.md:78-79`, and again in the alternatives table at `:457`). That is false for `context: fork`. Per the [skills reference](https://code.claude.com/docs/en/skills) (fetched 2026-07-24) a forked skill "won't have access to your conversation history" — it would satisfy the fresh-eyes requirement it was being rejected for. **The decision is unchanged.** The real basis was already recorded in the same PLAN's `[EXEC-SHAPE]` agent decision at `:550`: plugin agents start with fresh context, and the Brief requires a *named* subagent, which a forked skill is not. Both prose sites now carry that basis, and `design/design-resolution.md` — which stated the rule with no rationale at all — gains it. A wrong rationale in a design record propagates into later decisions that cite it, which is why the reason is corrected even though the outcome stands. ## Test plan - `grep -rn -i "degraded history" docs/topics/plugin-audit-port/` — no matches - The `[EXEC-SHAPE]` decision at `:550` is unmodified; the corrected text points at it rather than restating it - Docs-only change; no plugin version or CHANGELOG bump applies ## Related **One claim is deliberately attributed rather than asserted.** History inheritance is *documented* for the Agent tool's separate `fork` subagent type. #1258 reports empirically that Agent-tool forks did **not** inherit the conversation, contradicting that doc. The corrected text therefore says "documented for", not "does". The correction here does not depend on how #1258 resolves — the named-subagent requirement is the load-bearing basis, and the skills reference settles the `context: fork` half independently. Prior art, both closed: #1053 fixed the identical inversion in `docs-hygiene`'s `audit-derivability` rubric and evals; #1062 clarified its Hard Rules wording. This is the same class of error in a different file, not a duplicate. **Adjacent, non-overlapping:** #1252 also edits `plugin-audit-port/PLAN.md`, at `:551` — the table row directly below the `[EXEC-SHAPE]` row this PR points at. It does not touch that row, and this PR does not touch `:551`. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…durable-wiring # Conflicts: # docs/topics/plugin-audit-port/PLAN.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b9c276e95c
ℹ️ 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".
…n specifies Closes #1417 ## Summary `docs/conventions/topic-docs/README.md` specifies a required check that a merged PR carries no path under the contract-slice dir. The check was never built, so the convention has been unenforced for its entire life and 17 slices reached `main` — 6 of them on a single day. The only place `docs/topics/` reached CI at all was `scripts/docs-only-paths.txt`, as a docs-only ALLOWLIST entry, which makes such a PR cheaper to merge rather than blocking it. Evidence the rule is real and was being enforced by hand: PR #1286 was closed rather than merged, explicitly because its content was contract tier under `docs/topics/`. ## The deletion exemption The convention's own step 4 is a final commit that PRUNES the slice, so a literal "no path under the contract dir appears in the diff" reading would red-line the very commit that satisfies it. This gate keys on where a path LANDS: removals pass, a history-preserving `git mv` out of the contract dir (step 3's graduation) passes, and only an add, edit, or rename-into is red-lined. That requires knowing a path's status, which `--name-only` cannot express, so the gate reads `--name-status`. Deliberate deviation from the letter of the convention in service of its intent; the three-dot `base...HEAD` range is unchanged. ## Existing debt The 17 pre-existing slices are grandfathered by slug in `scripts/contract-slice-baseline.txt`, using the same stale-guarded idiom as `changelog-parity-baseline.txt` and `orphaned-fixtures-baseline.txt`: `--check` fails on an entry whose slice no longer exists, so an exemption cannot outlive its debt and a future slice cannot inherit a grandfathered slug. Graduating and pruning them is tracked separately. This is why the gate can land now instead of after a 71-file cleanup: it stops the bleed immediately while each slice graduates on its own PR, by whoever owns it. ## Verification The 11-case suite covers the add, pure-deletion, untouched, grandfathered, new-slug-despite-baseline, graduation-out, rename-into, unresolvable-base, live-baseline, stale-baseline, and usage paths. Measured against the four open PRs that carry `docs/topics/` paths, rather than asserted: #1318, #1252, and #1096 pass on their baseline exemptions; #1400 fails, correctly, because it adds two slices that are not pre-existing debt. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n specifies (#1429) Closes #1417 ## Problem `docs/conventions/topic-docs/README.md:305-309` specifies a required check that the net PR diff carries no path under the contract-slice dir: > 5. Enforcement: a required check that the net PR diff (`git diff --name-only base...head`) > contains no path under the resolved `<contract_dir>/**` (default `docs/topics/**`). It was never built. The convention has been unenforced for its entire life, and `main` currently holds **19** contract slices — **8 of them landed on 2026-07-25 alone**, two of those while this PR was open. The only place `docs/topics/` reaches CI today is `scripts/docs-only-paths.txt:43`, and it is there as a docs-only **allowlist** entry — which makes a PR confined to it *cheaper* to merge by skipping the heavy lanes. Nothing blocks it. The rule is real and people have been enforcing it by hand: **PR #1286 was closed rather than merged**, explicitly because its content was contract tier under `docs/topics/`. That is the cost of the missing gate — correct behaviour depending on whoever is looking remembering an unenforced rule, and 19 directories showing how reliably that scales. **#1400 merged while this PR was open**, landing two more slices. It was flagged there, and it is the sharpest available evidence for the gate: the failure mode is live, not historical. ## Why the gate permits deletions The convention's own step 4 is *a final commit that prunes the slice*. A literal reading — "no path under the contract dir appears in the diff" — would red-line the very commit that satisfies the convention. So the gate keys on where a path **lands**, not on whether it appears: | Change | Verdict | |---|---| | Delete under `docs/topics/` | pass — this is the prune step | | `git mv docs/topics/x/PLAN.md docs/adr/…` | pass — this is step 3's history-preserving graduation | | Add / edit under `docs/topics/` | **fail** | | Rename *into* `docs/topics/` | **fail** | | Diff never touches `docs/topics/` | pass | Knowing a path's status requires `--name-status`; `--name-only` cannot express it. That is a deliberate deviation from the letter of the convention in service of its intent, called out in the script header. The three-dot `base...HEAD` range is unchanged, so a slice `main` gained after a branch forked stays out of scope and no stale branch is forced to merge-from-main over someone else's violation. ## Why this lands before the cleanup `scripts/contract-slice-baseline.txt` grandfathers the 19 existing slugs, using the same stale-guarded idiom as `changelog-parity-baseline.txt` and `orphaned-fixtures-baseline.txt`: `--check` fails on an entry whose slice no longer exists, so an exemption cannot outlive its debt and a future slice cannot silently inherit a grandfathered slug. Exemptions are resolved from the **base revision**, not the working tree, so a PR cannot add a slice and grandfather its own slug in the same diff. The diff is judged against the union of the base and head contract roots, so a PR that relocates `contract_dir` cannot leave the root it selected uninspected either. Both bypasses were live in earlier pushes and were caught in review. The alternative — prune all 19 first, then gate — is a ~1.3 MB change requiring a graduation judgement on each slice by whoever owns it, and it would conflict every open PR that carries those paths. Gating first stops the bleed immediately while each slice graduates on its own PR at its own pace. The burn-down is #1419; each prune PR drops its own baseline line, and the stale guard means the debt cannot be quietly abandoned half-done. ## Verification `scripts/check-contract-slice-prune.test.sh` — 19 cases, all green: add, pure deletion, untouched tree, grandfathered slug, new slug despite a baseline, graduation out, rename in, unresolvable-base-ref (fail-closed, exit 2), live baseline entry, stale baseline entry, usage, self-grandfathering rejected, a pre-existing entry still exempting, `contract_dir` resolved from the concern file, a relocated root moving the gate's scope, a root-equivalent value exiting 2, a slug-less baseline surviving `set -u`, both base and head roots policed, and a grandfathered slice migrating to a relocated root. Four review findings were raised across two rounds and all four were reproduced before being fixed — two bypasses (self-grandfathering; a relocation leaving its own root uninspected) and two fail-open / crash defects (`contract_dir` ignoring the concern file; the gate aborting under `set -u` once the baseline empties, which is the exact end state #1419 drives toward). See the resolved threads; each carries its reproduction and the case that pins it. **Measured against the open PRs that actually carry `docs/topics/` paths, rather than asserted.** Because the gate reads the baseline from the base revision, these were run against a base that already carries it — the post-merge condition: | PR | Slice | Result | |---|---|---| | #1252 | `plugin-audit-port` | passes on baseline exemption | | #1096 | `fresh-eyes-checkpoint-audit` | passes on baseline exemption | | #1318 | `context-engineering-claude-5` + the two slices #1400 landed | fails until rebased onto a `main` carrying the updated baseline | #1318's failure is an artefact of it predating #1400's merge, not a defect: its branch adds those two files relative to its own fork point. Once rebased, they are on `main` and in the baseline, so they leave its diff entirely. The self-grandfathering bypass was verified closed by re-running the reviewer's own reproduction against the fix. Also verified: `shellcheck` clean, `actionlint` clean, `shfmt` clean, the org comment-hygiene policy reports zero violations in the new files, and both scripts carry the executable bit. ## Wiring `contract-slice-prune-gate` is added to `ci-status`'s `needs:` list. That aggregate derives its lane list from the needs graph, and `ci-status` is already a required status check on the ruleset, so the new gate becomes required with **no ruleset edit**. Job naming matches the existing precedent (`silent-skip-gate`, `orphaned-fixture-gate`, `changelog-parity-gate`). The self-test runs unconditionally so a broken gate cannot mask a regression; the PR-diff step is event-gated. ## Related - #1419 — graduate and prune the grandfathered slices. Each prune PR drops its own baseline line, and this gate's stale guard fails once an entry outlives its slice. Not closed by this PR. Its inventory needs updating to 19 once this lands. - #1400 — merged while this PR was open, landing two more slices; both added to the baseline as debt rather than treated as incoming work. Flagged there before it merged. - #1252, #1096 — open PRs carrying grandfathered slice paths; verified passing on their baseline exemptions. #1318 needs a rebase past #1400 (see Verification). - #1286 — closed by hand for carrying contract-tier content, which is the manual enforcement this gate replaces. - `docs/conventions/topic-docs/README.md` — the convention specifying this check as step 5. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compose both sides rather than take either:
- `apply reset` -> `apply defaults` (main's clean-break rename) now also reaches
the frontmatter argument-hint, the evals expectation, and the `apply` section
body that the textual merge left on the old token.
- context-guard's Purpose keeps main's rewritten narrow-write rationale and
widens "exactly ONE writable artifact" to the operator-home directory, which
is what this branch actually changed.
- rate-limit-guard's Purpose keeps main's three-unwritable-surfaces enumeration
and the headless reconfiguration recipe, and states separately why a shim —
not configuration — is what obliges the new `apply`.
- Both plugins land on 0.3.0: main took 0.2.0 (context-guard) and 0.2.1
(rate-limit-guard) while this branch was open, so the shipped entries move
under a new heading with main's releases preserved verbatim beneath.
Also addresses the four live review findings:
- The shim resolves the cache from the effective config root
${CLAUDE_CONFIG_DIR:-$HOME/.claude}, not $HOME. CLAUDE_CONFIG_DIR is
documented as relocating settings, session history AND plugins
(https://code.claude.com/docs/en/env-vars, fetched 2026-07-25), so a
$HOME-anchored shim resolved nothing forever for an operator running the
documented multi-account alias. Both shims, shim-revision 2.
- `check` unwraps recognized guard shims before composing, so a statusline
already wired through the sibling (or through itself) is not wrapped twice.
- The combined sibling wiring is gated on the sibling shim existing, because
the sibling shim is written by that plugin's own `apply` — naming a missing
path is the `bash <missing>` 127 failure this change exists to remove.
- Uninstall is ordered: unwrap `statusLine` first, then delete the directory.
The shim's own fallback cannot cover the reverse order; it lives in the file
being deleted.
Harnesses grew three cases each (relocated config dir, CLAUDE_CONFIG_DIR
beating a HOME cache, empty value falling back): 31/31 both plugins. Existing
suites unchanged and green: context-zone 44, context-guard tee 44,
rate-limit-guard tee 34. Two evals added per setup skill.
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Warning Automated review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
|
Warning Automated security review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07a0807853
ℹ️ 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".
| for cand in "$cache"/*/"$PLUGIN_NAME"/*/scripts/statusline-tee.sh; do | ||
| # An unmatched glob expands to the literal pattern; -f rejects it. | ||
| [[ -f "$cand" ]] || continue |
There was a problem hiding this comment.
Stop resolving tees from uninstalled cache entries
When an operator uninstalls either plugin while leaving the advertised durable wiring in place, Claude Code retains the now-orphaned cache version until garbage collection (the repository records seven days after update/uninstall in docs/topics/plugin-fleet-sync-skill/PLAN.md:29). This glob treats any remaining file as installed, so the shim continues executing the uninstalled tee and writing context/rate-limit snapshots instead of taking its documented passthrough fallback. Verify that the candidate belongs to an active install, or require unwiring before uninstall; the symmetric rate-limit shim has the same issue.
Useful? React with 👍 / 👎.
| **Unwrap before you compose.** `<current statusline command>` below means the operator's OWN | ||
| renderer, never the raw effective `command` string. Before substituting, strip every leading | ||
| guard-shim invocation from that string — `bash <path>/context-guard/bin/statusline-shim.sh` and | ||
| `bash <path>/rate-limit-guard/bin/statusline-shim.sh`, in whatever order they appear — plus any | ||
| legacy `bash <plugin-cache>/…/statusline-tee.sh` prefix, and treat what remains as the renderer. |
There was a problem hiding this comment.
Unwrap the generated sh -c adapter on reruns
When check is rerun on the shell-syntax form this skill itself generated, stripping only the guard prefix leaves sh -c '<original>', which these instructions then treat as the renderer and wrap again because it contains quoting. Applying the new edit therefore changes bash <shim> sh -c '<original>' into bash <shim> sh -c 'sh -c ...', and every subsequent check/application adds another shell, contradicting the stated idempotence and accumulating process and quoting overhead. Recognize and unwrap the generated sh -c adapter before recomposition; the rate-limit setup has the symmetric instructions.
Useful? React with 👍 / 👎.
…ing sh -c wrap (#1844) Fixes #1787 ## Summary Two findings that landed on #1252 after it merged, so the thread-resolution gate never saw them. Both were re-verified present on `origin/main` before this branch started, and both are fixed here with regression coverage. 1. **P1 — an uninstalled plugin's tee kept executing through the shim.** `resolve_tee()` picked the newest `scripts/statusline-tee.sh` by mtime, skipping only `temp_*` marketplace clones. It never asked whether the plugin was still installed, so an uninstalled plugin kept teeing and kept writing snapshots, with no signal to the operator. 2. **P2 — `setup` added one `sh -c` layer per run.** "Unwrap before you compose" stripped guard-shim prefixes but not the `sh -c '<escaped …>'` adapter the skill's own shell-syntax guard prints. ## Fix ### The orphan marker, not `installed_plugins.json` `claude plugin uninstall` does not delete the version directory. The plugins reference, under [Plugin caching and file resolution](https://code.claude.com/docs/en/plugins-reference) (fetched 2026-07-30), states: *"When you update or uninstall a plugin, the previous version directory is marked as orphaned and removed automatically 14 days later. The grace period lets concurrent Claude Code sessions that already loaded the old version keep running without errors."* Uninstall is named explicitly, so `scripts/statusline-tee.sh` stays on disk — and stays executable by an mtime-only shim — for that whole window. `resolve_tee()` now skips a candidate whose version directory carries the orphan marker. The issue asked for a **supported** signal, not the internal file the shim header deliberately rejected. That rejection stands, on both of its original grounds: `installed_plugins.json` is undocumented and reading it would put a `jq` spawn on every statusline refresh. The orphan marker beats it on both — the behavior it reports is documented, and the test is a shell builtin, so the file's pure-builtins invariant is preserved (`[[ -e … ]]`, no subprocess). **What is documented vs. what is measured**, recorded at the site rather than assumed: - The *marking* is documented (quoted above). - The marker's *on-disk spelling* is not. Measured on Claude Code 2.1.220 against a relocated `CLAUDE_CONFIG_DIR`: an uninstall writes `<version-dir>/.orphaned_at` holding an epoch-ms stamp and leaves `scripts/statusline-tee.sh` in place. Reproducible on any live cache — every superseded version directory carries the marker and the currently installed one does not. - A directory can also be marker-less while merely *staged* (a newer version fetched for a pending update), so absence of the marker is not a claim of installation. mtime still picks the winner among unmarked candidates, exactly as before. - **Fallback if upstream renames or drops the marker:** the test simply finds nothing and resolution degrades to today's mtime-only behavior — a stale tee, never a broken statusline. ### Unwrapping is now a fixpoint, not one pass "Unwrap before you compose" became two rules applied until a pass strips nothing: 1. guard-shim prefixes (unchanged); 2. a generated `sh -c '<single-quoted string>'` adapter with nothing after the closing quote **and whose carried string itself contains shell syntax** — unescaped back by dropping `sh -c` and the outer quotes and replacing `'\''` with `'`. Iterating matters: an operator may already carry several layers from earlier reruns, and a single peel over three layers leaves three. The shell-syntax-guard step now also states that it applies only to the *unwrapped* renderer. **The second condition on rule 2 is provenance, and it is load-bearing.** The first push of this branch peeled any bare `sh -c '<string>'`, and disclosed that as a deliberate, behaviorally equivalent narrowing. Review showed that claim was wrong, so it is now fixed rather than documented: an operator whose genuine renderer is `sh -c 'ulimit -n'` would have had the layer dropped, and since `ulimit -n` carries no shell syntax the guard would not restore it — leaving the shim `exec`-ing a shell builtin with no shell, so the statusline exits 127 instead of rendering. Requiring shell syntax in the carried string is a provenance test, not a heuristic: this skill emits the adapter **only** for a renderer that carries shell syntax, so an `sh -c` over a string carrying none cannot have come from it and is the operator's own. Both target cases still hold — every generated layer wraps something with shell syntax, so nested *generated* layers collapse until what remains is the operator's own, while the genuine-builtin case is preserved verbatim. A trailing word (`sh -c '…' extra`) marks a real command and is left alone. Stated precisely, because the distinction matters: the fixpoint does not always terminate at a bare renderer. Over a hand-written `sh -c 'sh -c '\''my-statusline --flag'\'''` the outer layer peels (its carried string contains quoting) and the inner one does not (`my-statusline --flag` carries no shell syntax), leaving exactly one layer. That is the correct outcome — the surviving layer is the operator's own — and it is still idempotent and non-compounding, which is what the issue asked for. `context-guard` 0.4.3 → 0.4.4 with a matching `## [0.4.4]` CHANGELOG entry. (The branch originally bumped 0.4.2 → 0.4.3; the shared `hook-utils.sh` sync claimed 0.4.3 on main while this branch was open, so it was renumbered after fast-forwarding rather than co-owning a released version.) ## Verification Regression coverage per the acceptance criteria — and the shim cases are proven **non-vacuous**, not merely green: | Evidence | Result | | --- | --- | | `bash plugins/context-guard/scripts/statusline-shim.test.sh` (post-fix) | **36 passed, 0 failed** | | Same suite against the **pre-fix** shim | **3 failed** — `want [] got [TEE:uninstalled]`, `[TEE:orphaned] does not contain [TEE:installed]`, `[TEE:orphaned] unexpectedly contains [TEE:orphaned]` | | Live-cache measurement of the marker | 5 superseded `context-guard` version dirs all carry `.orphaned_at`; the installed one does not | | Doc claim re-fetched this session | uninstall named explicitly in the quoted sentence | Two new shim cases: an uninstalled plugin's orphaned tee is not executed *and the statusline still renders with the wrapped exit code preserved*; and an orphaned directory loses to an installed sibling **even when it is newer by mtime**, so only the marker can decide the second case. The P2 fix is a prose-instruction change, so its regressions live where instruction behavior is graded — two evals, one per direction: - **id 8, `rerun-does-not-compound-the-sh-c-wrap`** — pins the exact rerun in the issue and asserts one `sh -c` layer, never a nested one. Traced by hand: `…shim.sh sh -c 'THEME=dark my-statusline --flag'` → rule 1 strips the shim → the carried string has an inline env assignment, so rule 2 recovers `THEME=dark my-statusline --flag` → second pass strips nothing → re-wrap reproduces the input byte-for-byte. - **id 9, `genuine-sh-c-renderer-is-not-peeled`** — the opposite direction, added in response to review. `sh -c 'ulimit -n'` carries no shell syntax, so rule 2 does not fire and the adapter survives into the printed wiring. ## Review findings addressed on this branch | Finding | Disposition | | --- | --- | | Codex P2 — "Preserve genuine `sh -c` renderers" (`sh -c 'ulimit -n'` peeled to a broken `exec`) | **Fixed**, not just documented. Rule 2 now requires shell syntax in the carried string as a provenance test; eval 9 pins it. | | Claude review — garbled fallback sentence in the 0.4.4 changelog entry | **Fixed.** Rewritten to "should upstream rename or drop the marker, resolution degrades to exactly what it does today — a stale tee, never a broken statusline." | Gates, all run from the worktree root against `origin/main`: | Gate | Result | | --- | --- | | `shellcheck` (shim + test) | clean | | `scripts/check-shell-portability.sh origin/main` | PASS — 2 files, no unexcused GNU-only constructs | | `scripts/check-changed-skills.sh origin/main` | PASS — 0 errors, 2 warnings (both pre-existing: soft line target, no Gotchas surface) | | `scripts/check-changelog-parity.sh --check` | PASS | | `scripts/check-changelog-parity.sh --check-bump origin/main` | PASS | | `scripts/check-changelog-parity.sh --check-order` | PASS — 71 changelogs, newest-first, no duplicate versions | | `markdownlint-cli2 "plugins/context-guard/**/*.md"` | PASS — 0 issues, 4 files | | `scripts/check-cross-plugin-source-drift.sh --check` | PASS | | `scripts/check-silent-skips.sh` | PASS | | `scripts/validate-plugins.sh` | PASS — manifests + catalog | Gate scoping notes, stated rather than assumed: `check-silent-skips.sh` reads only `plugins/*/hooks/*.sh`, so the new `[[ -e … ]] && continue` in `scripts/statusline-shim.sh` is outside its corpus by construction — it is not an unreported silent skip but a resolution filter in a non-hook script, documented inline at the site. `check-cross-plugin-source-drift.sh` passes because the two plugins' shims are already not byte-identical and are correctly unregistered. ## Related - Refs #1252 — the merged PR both findings were stranded on. Its two threads are resolvable only after this merges; recorded here as the remaining acceptance item rather than actioned on the branch. - Refs #1777 — the stranded-findings sweep that surfaced them. - **Follow-up, not fixed here:** `plugins/rate-limit-guard/scripts/statusline-shim.sh` carries the identical `resolve_tee()` defect — same glob, same `temp_*`-only filter, no orphan check — so an uninstalled `rate-limit-guard` keeps teeing for the same ~14-day window. It is out of scope for #1787, which is scoped to `context-guard`, and porting it would pull a second plugin's version bump, changelog, and test suite into this diff. The two shims are deliberately *not* a registered byte-identical cluster (they differ in plugin name and header prose), so no drift gate will surface it — filing it is the only thing that keeps it visible. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…usline renderer (#1999) ## Summary `/context-guard:setup check` printed an operator's own `sh -c '<command>'` renderer back to them wrapped in a second one — `sh -c 'sh -c '\''ulimit -n'\'''` — adding one more shell on every statusline refresh. PR #1844 fixed the mechanism the finding named: unwrap rule 2 (`skills/setup/SKILL.md:143-153`) correctly declines to peel an `sh -c` the operator wrote themselves, because the string it carries holds no shell syntax. But the finding's complaint kept reproducing by a second path. The *preserved* renderer then reached the shell-syntax guard, which listed bare **quoting** among the triggers needing an adapter — so it matched on its own quote characters and was wrapped anyway. The escape hatch that was supposed to stop that had a true condition and a **false** rationale: it claimed such a command meant "rule 2 above having been skipped", when rule 2 had in fact run and declined. A finding is discharged when its complaint stops reproducing, not when the line it pointed at changes. Decisive proof it had not: a faithful implementation of the prose failed this skill's own **eval 9** (`skills/setup/evals/evals.json:107-118`), whose fourth expectation is "prints exactly one shim invocation and exactly one `sh -c` layer". ## What changed **Quoting is no longer a trigger, and the trigger test is scoped to top-level syntax.** The `statusLine` `command` field "runs in a shell" ([docs](https://code.claude.com/docs/en/statusline), fetched 2026-08-07), so that shell splits the line into words and consumes the quotes before `statusline-shim.sh` sees ARGV, and the shim `exec`s those words unchanged. A quoted argument therefore already survives the plain wrapped form intact — including an operator's `sh -c '<string>'`, where `sh` is the executable and `-c` and the carried string are two ordinary ARGV words. The guard now fires only on syntax no ARGV word can express — an inline env assignment, a pipe, `&&`, `||`, `;`, a trailing `&`, a redirection — **and only where it stands unquoted at the top level**. That second clause matters on its own: without it, "carries shell syntax" reads as a substring test, and an implementer meeting `sh -c 'a | b'` at the guard sees a pipe and wraps it, reproducing the double wrap by a third path. **Rule 2's provenance test is now three explicit branches, because scoping the guard silently rescoped it too.** Rule 2 cites the guard to define "carries shell syntax", so narrowing the guard narrowed rule 2 with it — and a wrap whose syntax sits inside inner quotes then looked operator-written. Two of the three branches are therefore keyed to the **shape** of the carried string, not the syntax in it, and are explicitly exempt from the top-level scoping: - **A — the carried string is itself an `sh -c '<string>'`.** Always a generated layer: an operator's renderer is at most one `sh -c` deep. Without this the peel stops one layer early and hands back the two-layer wrap it exists to collapse. - **B — the carried string begins with a guard-shim prefix.** This skill never puts a shim inside an adapter. Sealed there it is invisible to rule 1, which strips only *leading* prefixes, so the composed wiring named the sibling shim a **second** time and ran its tee twice per refresh — the `context → rate → rate → renderer` duplication this skill exists to prevent, reachable for anyone who wired `rate-limit-guard` first and ran the pre-0.4.8 guard. - **C — the carried string is a command the guard would wrap.** The only shape this skill's own adapter ever carries. Absent all three it is the operator's and is preserved. One shape stays ambiguous **by design** — a single `sh -c` over a merely-quoted command, which the buggy guard also emitted and which carries no evidence either way. It is preserved: one spurious shell per refresh is cheaper than a statusline broken by peeling on a guess. The prose says so rather than implying a re-run cleans it up. **The hatch's condition and rationale now agree**, because the hatch is gone and the guard's own condition does the work. The idempotency claim is scoped to what is actually true: for an input that is itself `sh -c '<string>'`, the two tests leave exactly one layer. That does **not** generalize to a layer count — a plain renderer takes none, and `sh -c 'ulimit -n' && echo ok` correctly takes **two**, since `&&` cannot be an ARGV word and peeling the inner `sh -c` would strand the builtin. What is invariant is that peel and wrap are inverses, which is what makes a re-run byte-identical at whatever count the renderer needs. **Three evals now pin this.** Eval 10 (`multiple-generated-layers-collapse-in-one-run`) was added earlier on this branch and deleted by `81c03090e7` with no rationale in the commit message or the CHANGELOG; it covers the nested-layer case exactly, and the scoping change broke it. It is restored with a fifth expectation pinning branch A. Eval 11 (`adapter-hiding-a-sibling-shim-is-peeled`) is new and pins branch B. **`check`'s shim-drift report stopped claiming a differing installed copy is harmless.** It said an older or hand-edited copy "still resolves the newest tee", which stopped being true when `# shim-revision: 3` added the orphan skip: a copy predating it picks by mtime alone, so it also resolves a tee left behind by an uninstalled plugin and keeps teeing for the whole ~14-day grace window. It now states which of the two behaviors the installed copy has. `0.4.7` → `0.4.8` with the matching CHANGELOG entry. ## Verification - Traced end to end for eval 9's input `sh -c 'ulimit -n'`: rule 1 finds no shim prefix; rule 2 declines on both branches (carried string is not an `sh -c` shape, and the guard would leave it alone); the guard finds no unquoted top-level trigger; the plain wrapped form is printed — one shim invocation, one `sh -c` layer. All four expectations met. - Eval 10's input `bash <shim> sh -c 'sh -c '\''THEME=dark my-statusline'\'''`: rule 1 strips the shim, rule 2's nested branch peels the outer layer, rule 2's guard branch peels the inner one, a third pass strips nothing, the guard fires once on `THEME=dark my-statusline`. One layer. - Eval 11's input (a `rate-limit-guard` shim sealed inside a preserved adapter): branch B peels the adapter, exposing the shim to rule 1, which strips it. The combined wiring names each shim exactly once. - Eval 8 is unaffected: rule 2 peels, the env assignment lands unquoted at top level, the guard fires once, output is byte-identical to the wiring already in settings.json. - No eval was loosened; one was restored and strengthened, one added. Evals 1-7 carry no renderer whose behavior the trigger-list change moves. - **A fresh-context verifier CHALLENGED this fix twice, and was right both times.** Round one caught that the guard scoping had silently regressed the nested-layer case; round two caught a false idempotency invariant and the sealed-sibling-shim duplication. Branches A and B, both restored and new evals, and the scoped invariant all come from those challenges. - Gates green from the worktree root: changelog parity (`--check`, `--check-order`, `--check-bump` against `origin/main`), `check-changed-skills.sh` (context-guard/setup PASS, 0 errors), orphaned-fixture gate, shell-portability, skill-portability, markdownlint, and the context-guard suite (shim 36, tee 47, zone 73 — 0 failures). - SKILL.md is 391/500 lines. ## Related - Reopens review finding `PRRT_kwDOTCGFQM6Tzj7l` on #1252, which a prior pass closed as already-fixed and a fresh-context verifier correctly challenged. - Builds on `3cbfaa89ae` (#1844), which fixed unwrap rule 2 and the shim's orphan skip. That behavior is untouched here. - Out of scope, filed for follow-up: `plugins/rate-limit-guard/skills/setup/SKILL.md` still lists bare quoting as a trigger and has no adapter-peel rule at all, so the sibling skill reproduces this same finding on its own surface. Not touched here. No linked issue --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Both guard plugins' statusline wiring pointed at a version-pinned plugin-cache path.
${CLAUDE_PLUGIN_ROOT}changes on every plugin update and the old version directory is pruned ~14 days later (plugins reference, fetched 2026-07-24), so that wiring stops teeing silently at the next version bump and then takes the operator's whole statusline down when the path disappears (bash <missing>→ 127, and the wrapped statusline never runs).This ships
scripts/statusline-shim.shin context-guard and rate-limit-guard as the durable wiring target: the operator wires the shim once, and it resolves whichever tee version is installed at run time.0.9.0sorts after0.10.0lexically and version dirs carry no semver guarantee), skipping transienttemp_*cache clones. The marketplace name is never assumed, so the shim stays repo-agnostic.exec bash <tee> "$@"; no tee →exec "$@"(statusline unaffected, only the snapshot is lost); no tee and no wrapped command → one diagnostic line instead of a blank bar, never an emptyexec.-nt), no subprocess. Measured at ~14 ms added per refresh.Setup-contract changes
context-guard:setup applyinstalls the shim alongside the zones.json seed/repair — a widening of its previously zones-only scope (the A4 EXEC-SHAPE entry is annotated in PLAN.md).rate-limit-guard:setupgains anapplyaction for the first time; it writes the shim and nothing else.check's drift test is a plaincmp. Idempotent.checkgained an installed-shim state, and now classifies a statusLine wired to a plugin-cache path as LEGACY regardless of whether that file currently exists (the old rule only compared paths).settings.json.Security
New write surface (an executable into operator home) → delta security review recorded in
docs/MIGRATION-PLAYBOOK.md, verdict ACCEPT. The copy is byte-identical to already-reviewed bundled code, lands in each plugin's already-accepted operator-home carve-out, and is inert until the operator wires it — so the base record's "no kill switch needed: nothing runs unless the operator wires it" justification survives verbatim.${CLAUDE_PLUGIN_DATA}was considered and rejected as the shim's home (deleted on uninstall → reintroduces the exact 127 failure; per-plugin-identity path would hardcode the marketplace name into operator settings).Composition + measured cost
The tees are transparent wrappers, so they nest — each through its own shim, innermost command owning stdout and the exit code. Both setup skills print that combined form and state the measured cost: ~0.6–0.9 s per statusline refresh per tee on Windows/Git Bash (process-spawn bound:
jq+date; the shim itself is ~14 ms). Display latency, not input latency.Verification
temp_*clones skipped even when newer, marketplace name not assumed, sibling plugins ignored), transparency (stdin bytes, stdout, exit codes 0 and non-zero, both with and without a tee), the no-tee/no-args case, unsetHOME, and two-shim chaining including a missing middle tee.shellcheckclean;validate-plugins.sh, cross-plugin drift, changelog parity, skill-portability, skill leaf names, silent skips, hook argv all pass;check-changed-skills.shPASS on both setup skills (0 errors).Both plugins bumped to 0.2.0 with changelog entries.
Related
Closes #1251