Repository navigation
fix(repo-hygiene): close the clean-guard gaps Security & Cost found on #8168 #8622
Copy link
Copy link
Closed
Copy link
Labels
agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.Fully specified and briefed; eligible for autonomous pickup from the frontier.area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.Security-relevant: vulnerability, hardening, or disclosure follow-up.priority: mediumReal value, no hard deadline; normal backlog flow.Real value, no hard deadline; normal backlog flow.status: readyTriaged, unblocked, and fully specified; eligible to pick up.Triaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.A briefed fix or small feature; blast radius bounded by the brief, tests exist.
Description
Refs: #8168
GitHub Security & Cost passed #8168 at b97ad7b with only low-severity findings. These are the three optional items, filed so the PR can go to its owner merge without another review round.
Findings
- P4:
destructive-guard.shargv_segmentlets nested commands through. Matching on parsed words (rather than the raw command, as the base did) lets these pass:x="$(git stash drop)","$(rm -rf x)", backticks inside double quotes,$()in an unquoted heredoc,bash -cwrapped in sudo/timeout/env/xargs,eval,ssh "…",bash <<< "…",printf … | bash, andgit --git-dir … reset --hard.x="$(…)"is a natural spelling, not obfuscation.
Fix: re-parse any word holding an unescaped$(or backtick, and everyevaloperand, as its own command line. - P4 (low confidence): the guard is inert when the hook sees a different
CLAUDE_PLUGIN_DATAthan the skill used. Fail closed unless it names repo-hygiene, asclean-batch.shalready checks. Afteropen, run a probe command the guard must block. - P5:
source "$SCRIPT_DIR/hook-utils.sh"has no|| exit 2. If the file is missing, the guard allows everything.
Out of scope: deleting the marker (or clean-run-marker.sh close) unlocks the guard, but the CLEAN_GUARD_ACK=1 prefix already gives the same power, so hardening it buys nothing.
Acceptance
- A stay-loud test for each form in item 1, failing before the fix.
- A test where a mismatched
CLAUDE_PLUGIN_DATAblocks instead of allowing. - A missing
hook-utils.shexits 2.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.Fully specified and briefed; eligible for autonomous pickup from the frontier.area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.Security-relevant: vulnerability, hardening, or disclosure follow-up.priority: mediumReal value, no hard deadline; normal backlog flow.Real value, no hard deadline; normal backlog flow.status: readyTriaged, unblocked, and fully specified; eligible to pick up.Triaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.A briefed fix or small feature; blast radius bounded by the brief, tests exist.
Type
Fields
No fields configured for Bug.
Activity
github-actions commentedon Oct 20, 2026
Proposed work class: work-class: scoped
Basis: automated intake triage; the lanes App applied this class.
Size: Small
Complexity: Medium
Uncertainty: Known
Flags: none
Blocked by: none
#8622 (comment)
The decision settles the open questions: all item-1 forms are covered, a mismatched CLAUDE_PLUGIN_DATA fails closed using the clean-batch.sh segment rule, and hook-utils.sh is sourced with
|| exit 2. No questions remain. Related: #9779 edits the same matchers, so the two changes may need coordination.Automated triage, applied by the lanes App.
github-actions commentedon Oct 26, 2026
The triage sweep dispatched intake triage for this issue (attempt 1 of 2).
melodic-automation-lanes commentedon Oct 26, 2026
The latest triage verdict holds nothing: the issue is at
status: ready.kyle-sexton commentedon Oct 26, 2026
Decision recorded. The owner answered in the Refinement and Issue Triage sessions (2026-10-25), deciding the open items below on the facts:
No owner ledger (
.work/*/interview-checklist.md,.work/gh-monitor-owner-decisions/) names this issue. The body rates all three findings P4 or P5, so this is no high security risk. Every change below hardens the guard; none narrows what it blocks.All three gaps are still open on main. A probe of the origin/main scripts (a live marker for session
s1,CLAUDE_PLUGIN_DATAending inrepo-hygiene) exited 0, allowing the command, for every item-1 form:x="$(git stash drop)","$(rm -rf x)", a backtick inside double quotes,$()in an unquoted heredoc,bash -cbehindsudo,timeout,envandxargs,eval,ssh h "…",bash <<< "…",printf … | bash, andgit --git-dir /r/.git reset --hard. Baregit stash dropandgit reset --hardexited 2. ACLAUDE_PLUGIN_DATAnaming another plugin exited 0. Withhook-utils.shremoved,git stash dropexited 0 afterhook::bash_parse_segments: command not found.Decided: Every form the body lists, since its Acceptance asks for a stay-loud test for each one. The body's fix line (re-parse a word holding an unescaped
$(or backtick, and everyevaloperand) does not reach the wrapper,ssh,<<<, pipe-into-shell and--git-dirforms, so the fix covers those too. The script's threat-model note says these forms bypass the guard "by design" (plugins/repo-hygiene/skills/clean/scripts/destructive-guard.sh:20-29, and:60-62for a global option with a separate value). That note changes with the fix. It still calls the guard a best-effort net, not a security boundary, and still namespermissions.denyas the durable boundary. Precision holds: the existing stay-quiet cases for quoted text (destructive-guard.test.sh:68-79) and the benignbash -coperand (:88) still pass.CLAUDE_PLUGIN_DATAfail closed?Decided: The guard treats a
CLAUDE_PLUGIN_DATAwhose last path segment is notrepo-hygieneorrepo-hygiene-*as unresolved, using the same segment rule asclean-batch.sh:155-161. That reaches the existing fail-closed path: no marker path, so the guard stays armed (destructive-guard.sh:334-338). The test for an unset data dir (destructive-guard.test.sh:215-220) is already in place. The suite's own fixture setsCLAUDE_PLUGIN_DATA="$TEST_ROOT/data"(destructive-guard.test.sh:23), so it moves to a directory namedrepo-hygiene. The body's other suggestion, a probe command afteropen, is not added. The fail-closed check removes the cause, and a probe would add a step to every run to detect the same thing.Decided: As the body says.
destructive-guard.sh:341sourceshook-utils.shwith no|| exit 2.Out of scope, per the body: the marker-deletion unlock, which
CLEAN_GUARD_ACK=1already equals.Acceptance criteria: (converted from the body's Acceptance list, wording unchanged except "test" made specific to
destructive-guard.test.sh, and the precision and comment lines added from decisions 1 and 2)destructive-guard.test.shhas a stay-loud test for each form in item 1, and each fails before the fix.CLAUDE_PLUGIN_DATAblocks instead of allowing.hook-utils.shexits 2.destructive-guard.test.shstill pass.destructive-guard.shno longer lists the item-1 forms as bypassed by design, and still calls the guard a best-effort net withpermissions.denyas the durable boundary.melodic-automation-lanes commentedon Oct 26, 2026
Refinement. A read-only run explored this repository, searched its issues and pull requests, and read the allowed docs to draft what follows. On an issue a listed human filed, it counts as the issue's spec once posted, while unedited; it is still model text, so check it.
Intended outcome: Harden plugins/repo-hygiene/skills/clean/scripts/destructive-guard.sh so nested and wrapped destructive forms are blocked, a CLAUDE_PLUGIN_DATA not naming repo-hygiene fails closed, and a missing hook-utils.sh exits 2.
Acceptance criteria
Related work
Sources read
Automated refinement by the lanes App. Intake triages the issue again with it.