fix(overengineering): never give a detached checkout a branch identity (0.3.0) - #3222
Conversation
|
Claude finished @kyle-sexton's task in 3m 16s —— View job CI security review
This PR is documentation/instruction changes for three One finding from reviewing the new fallback path this fix adds, posted inline on IMPORTANT — The new "prefer a logical ref where the environment supplies one" fallback ( No other security-relevant issues found — no secrets handling, no permission-widening, no unpinned dependencies, no unsafe GitHub Actions patterns in this diff. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 930c518ba4
ℹ️ 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".
|
Last security-reviewed head: |
|
Claude finished @kyle-sexton's task in 3m 21s —— View job Code Review Complete
SummaryReviewed the diff ( No blocking findings. The precompute swap (
One pre-existing item, out of scope for this lane: No inline comments were posted since no changed line warranted one under the high-signal bar. |
|
Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
ab84548 to
515862a
Compare
2ec4734 to
a05d882
Compare
…y (0.2.3) `overengineering:audit` and `overengineering:realign` precomputed the current branch with `git rev-parse --abbrev-ref HEAD`, which answers the literal string `HEAD` when HEAD is detached — the ordinary shape for a scheduled CI runner. Three consequences followed, all of them silent: the audit wrote `branch: HEAD` into the findings artifact, giving it an identity that is not a branch; realign's branch-match refusal compared `HEAD` to `HEAD`, passed by construction, and could execute one ref's findings against another ref's surface, in the only mutating skill this plugin has; and the `<branch-slug>` home key resolved every detached ref to one directory, so unrelated runs shared an artifact. Both precomputes now use `git symbolic-ref --quiet --short HEAD`, which fails rather than inventing a name, matching the `delta` lane that already resolved identity this way. All three skills now agree on one resolution contract: prefer a logical ref where the environment supplies one naming a branch, with no vendor's variables named or assumed, and otherwise treat the identity as unresolved. On an unresolved identity the audit persists no findings artifact at all while still walking and still emitting its inline summary, and realign refuses — both when its own checkout has no identity and when the artifact it finds carries `branch:` absent, empty, or `HEAD` — never reaching the degenerate comparison. The artifact contract and the home-key binding state the case rather than leaving it to be inferred. `context/findings-artifact.md` documents `branch:` as resolved with `git symbolic-ref` and never the literal `HEAD`, and argues why omitting the key is deliberately not the remedy: an artifact whose identity cannot be established is one realign must refuse anyway, so writing it only moves the failure later. `reference/topic-docs.md` records that an unresolved identity keys no home and does not run the rung order, rejecting each substitute on its own terms — `HEAD` is one directory for every ref, a commit sha is a fresh home every commit that never resumes, and a fixed literal is `HEAD` under another name. Adds eval coverage for the detached path on both skills, which previously existed for neither: the audit's refusal to persist, its use of a supplied logical ref, realign's stop before reading the artifact, and its refusal of an artifact carrying `branch: HEAD`. Closes #3149 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtbWChCVfUWAv1Pi5Qk2hA
…nconsistent (0.3.0) Independent review of the first commit found four surfaces that either contradicted the new behavior or misstated it. `skills/audit/context/report-template.md` owns the read-only disclosure line and still declared the findings artifact written "always", hardcoding the form that names a resolved path. A run following the document the skill names as the owner of output shape would announce a file it did not write. It now carries the no-artifact variant and the inline summary no longer presumes the artifact was written first. `skills/delta/SKILL.md` closed its own detached-checkout section with "the audit still runs, exactly as it otherwise would" — true when it was written, false once the audit began declining its write on an unresolved identity. Its step 4 would have gone looking for a post-run artifact that no longer exists. Both now state the no-artifact case. The lane's behavior is unchanged; only the account of the audit's behavior was made wrong by the fix. Identity resolution is now a stated body step in `audit` and `realign` rather than a precompute alone. A worktree-isolated or dispatched executor may decline to inject the precomputed context block, and that is the same unattended context in which a detached checkout is most likely — so a change confined to the precompute line would verify green on an attached local checkout and do nothing in the environment the defect actually lives in. Corrects three claims in the changelog that did not survive checking: the surviving `git rev-parse --abbrev-ref HEAD` sites outside this plugin number seven across six files rather than four; `babysit-prs` parameterizes a same-session checkout with its captured value, which is a different defect class rather than no defect; and `mutation-testing:audit` reaches the same identity-collapse through an empty `git branch --show-current` result, so the headline is narrowed to the `HEAD` form this release fixes. Adds the detached case to `skills/delta/context/recurring-wiring.md`, the wiring document read by exactly the population this affects, and narrows three eval prompts that asserted a detached checkout without establishing one. Version moves to 0.3.0 rather than a patch: a run that previously wrote a file now writes none, and the artifact contract gained an obligation consumers can depend on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtbWChCVfUWAv1Pi5Qk2hA
…uncapped Honor the 3149 review: strip refs/heads/ before check-ref-format --branch, reject "."/"..", distinguish no-checkout from detached-in-repo, and only cap "top findings" when an artifact was actually written. Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
a05d882 to
a861c92
Compare
|
Claude finished @cursor[bot]'s task in 4m 27s —— View job Code Review Complete
SummaryThe prior code-review pass already covered commits 1–2 (through One finding, posted inline on No other issues found in this commit's changes — the normalize/validate logic is applied consistently between |
| - **`audit` and `realign` gave a detached checkout a branch identity, collapsing every ref onto one | ||
|
|
||
| - **`audit` and `realign` gave a detached checkout a branch identity, collapsing every ref onto one |
There was a problem hiding this comment.
Malformed/duplicated changelog entry — orphaned bullet fragment.
Line 17 is a truncated duplicate of the bullet that correctly starts on line 19: same text ("audit and realign gave a detached checkout a branch identity, collapsing every ref onto one"), but cut off mid-sentence with an unclosed ** bold marker and no content, followed by a blank line, then the real entry repeats the identical opening and continues correctly.
- **`audit` and `realign` gave a detached checkout a branch identity, collapsing every ref onto one
- **`audit` and `realign` gave a detached checkout a branch identity, collapsing every ref onto one
(#3149).** Both skills precomputed the branch with `git rev-parse --abbrev-ref HEAD`, which answers
Line 19 onward is pre-existing content from an earlier commit in this PR (unchanged); only line 17 (plus its trailing blank line 18) is newly introduced by this commit and appears to be leftover text from an edit that didn't get cleaned up. It renders as a stray, content-free bullet item with a dangling unclosed bold span directly above the real entry.
Suggest deleting lines 17–18 so the "Logical-ref fallback…" bullet (lines 10–15) is immediately followed by the "audit and realign gave a detached checkout…" bullet (line 19) with no orphaned fragment between them.
Summary
overengineering:auditandoverengineering:realignprecomputed the current branch withgit rev-parse --abbrev-ref HEAD, which answers the literal stringHEADwhen HEAD is detached —the ordinary shape for a scheduled CI runner.
HEADlooks like a branch name, is the same stringfor every ref, and compares equal to itself, so three consequences followed and all of them were
silent:
auditwrotebranch: HEADinto the findings artifact, giving it an identity that is not abranch.
realign's branch-match refusal comparedHEADtoHEAD, passed by construction, and couldaccept another ref's artifact as this ref's own — in the only skill in this plugin that mutates
anything, where per-item human consent is spent down a rollback ladder as far as deletion.
<branch-slug>memory-tier home key resolved every detached ref to one directory, sounrelated runs shared a single artifact and merged each other's carried-forward operator
judgments.
The
deltalane already resolved identity correctly withgit symbolic-refand could not fix anyof these from where it sits, which is why this was filed separately from #3146.
Fix
Both precomputes now use
git symbolic-ref --quiet --short HEAD, which fails rather than inventinga name, and emit the same sentinel string
deltaalready used. All three skills now agree on oneresolution contract: prefer a logical ref where the environment supplies one naming a branch — with
no CI vendor's variables named or assumed — and otherwise treat the identity as unresolved.
On an unresolved identity:
auditpersists no findings artifact at all. Not the file withbranch:omitted, not aplaceholder value, not a file at a substitute home. The walk still runs and the inline summary is
still emitted, so the operator still gets the findings; only the persisted write is declined, and
the read-only disclosure line says that instead of naming a path.
realignrefuses, in both unresolved states: when its own checkout has no identity (it stopsbefore reading the artifact at all) and when the artifact it finds carries
branch:absent,empty, or
HEAD. Neither path reaches the comparison. The refusal is a positive preconditionrather than a consequence of the file being missing, which also renders any stale pre-fix
artifact already on disk unreachable without a cleanup step.
Identity resolution is a stated body step, not only a precompute. A worktree-isolated or
dispatched executor may decline to inject the precomputed context block entirely — and that is the
same
unattendedcontext in which a detached checkout is most likely — so both skills state theresolution command in the step that uses it and treat the precompute as a convenience that may be
absent. Confined to the precompute line, the fix would have verified green on an attached local
checkout and done nothing in the environment the defect actually lives in.
Two contract documents now state the case rather than leaving it to be inferred.
context/findings-artifact.mddocumentsbranch:as resolved withgit symbolic-refand never theliteral
HEAD, adds a "No branch identity, no artifact" section, and carries a per-skillobligations row.
reference/topic-docs.mdrecords that an unresolved identity keys no home and doesnot run the rung order, rejecting each substitute on its own terms:
HEADis one directory forevery ref, a commit sha is a fresh home every commit that never resumes, and a fixed literal such as
detachedisHEADunder another name.Three adjacent surfaces were made inconsistent by the change and are corrected in the same PR.
skills/audit/context/report-template.md, which owns the read-only disclosure line, still declaredthe artifact written "always" and hardcoded the path-naming form.
skills/delta/SKILL.mdclosed itsown detached section with "the audit still runs, exactly as it otherwise would" — true when written,
false once the audit began declining its write — and its step 4 would have looked for a post-run
artifact that no longer exists;
delta's behavior is unchanged, only its account of the audit's.And
skills/delta/context/recurring-wiring.md, the wiring document read by exactly the affectedpopulation, now names the detached case and the logical-ref remedy.
The
branch:decision was made by an independent panel, not by the implementing agent, and isrecorded in full on the issue with its reasoning, its rejected alternatives, and its caveats:
#3149 (comment).
Two fresh-context agents on different models, spawned separately with the implementing agent's
rationale withheld, independently reached the same answer as that agent's own prior read.
Verification
The behavior the fix turns on, observed directly. The marketplace's own primary clone happens to
sit detached, so both forms were run against a real detached checkout and against an attached one:
git rev-parse --abbrev-ref HEADgit symbolic-ref --quiet --short HEAD## HEAD (no branch))HEADfix/3149-…)fix/3149-…fix/3149-…The two forms agree exactly where a branch exists and diverge exactly where one does not, which is
the whole of the guard.
git branch --show-currentwas checked on the same detached checkout andreturns an empty string at exit 0 — the basis for the cross-plugin finding below.
Repo gates. This plugin ships no scripts and no
*.test.sh, so its coverage mechanism is theeval sets plus the shared static gates. All were re-run after the review fixes:
check-skill.sh—audit,realign, anddeltaeach PASS with 0 errors. The warnings eachreports (description length, the 200-line soft target) are pre-existing on
main.check-evals-quality.sh— PASS, 0 warnings across both eval sets.check-jsonschemaagainstplugins/skill-quality/reference/evals.schema.json— valid.check-skill-portability.sh— no unexcused coupling tokens.markdownlint-cli2— 0 issues across all eight changed markdown files.Eval coverage for the detached path, which existed for neither skill before. Four cases added:
auditdeclining to persist on a detached runner;auditpreferring an environment-suppliedlogical ref and naming its origin;
realignstopping before it reads the artifact when its owncheckout is detached; and
realignrefusing an artifact carryingbranch: HEAD. Each promptnarrates the checkout state it depends on, following the convention the existing shallow-clone case
already uses, so none asserts a condition it never establishes.
Independent review. A fresh-context reviewer with the author's rationale withheld checked the
change against the issue's five acceptance criteria and returned NOT READY on the first commit. Its
blocker (the stale
report-template.md) and every should-fix (the cross-plugin count, themutation-testingoverclaim, thebabysit-prscharacterization, three under-specified evalprompts, the missing wiring guidance) are addressed in the second commit. The version moved to
0.3.0 rather than a patch on its recommendation: a run that previously wrote a file now writes none.
Related
Closes #3149
deltalane PR where this was found; carries the delta-side defense this changeconverges
auditandrealignonto.Do other plugins share the pattern? No other plugin exhibits the
HEADidentity-collapse. Therepo-wide precompute boilerplate uses
git branch --show-current, which returns an empty string ona detached checkout rather than
HEAD. The literalgit rev-parse --abbrev-ref HEADform survivesat seven occurrences across six files outside this plugin:
claude-ops' two skill-usage telemetryhooks,
source-control'sbabysit-prsparking-branch capture andworktreecurrent-branchdisplay, and three test-scaffolding sites in
guardrails'stale-path-verify.test.shandsource-control'sworktree-create.test.sh. None compares the value against a stored identity todecide whether a mutation may proceed.
babysit-prsis the closest call and is still a differentdefect class — its captured value parameterizes a same-session
git checkout, so a detached capturefails to restore the starting commit rather than authorizing another ref's work. All are left as-is
deliberately. This corrects the triage note, which recorded two of these sites rather than six.
One neighbor has the same shape under a different string, and is out of scope.
mutation-testing:auditdocumentsbranch:asgit branch --show-currentverbatim(
skills/audit/context/persist-findings.md:202) and ships no script guarding the empty result, so adetached run there writes an empty identity that compares equal to itself. Three sibling
findings-writers (
ai-slop,docs-hygiene:audit-noise,claude-config:audit-instructions) alreadyguard that case and exit rather than persist. Recorded rather than fixed — this issue is scoped to
one plugin — and worth its own item.
Adjacent pre-existing condition, deliberately not fixed here.
scripts/check-skill-precompute-compose.shreports bothaudit/SKILL.mdandrealign/SKILL.mdascarrying a git command plus more than one injection line (#1619). Both files already had two
precompute lines on
main, so this change neither introduces nor worsens it, and the gate iswarn-only in CI pending that remediation wave. Making resolution a body step is what keeps this fix
correct in the meantime, since a refused precompute block no longer means an unguarded identity.