Post-use behavioral audit of repo-hygiene:clean 0.10.11, produced by plugin-quality:audit
after driving the skill through a full fleet cleanup: 65 working trees, 17 object stores,
14 GitHub repos, 830 branches, on Windows 11 / Git Bash + PowerShell 7.6.5.
Findings were produced by a fresh-context auditor reading the installed source, then the three HIGH
findings were independently re-verified against source by the dispatching session before filing.
Sealed evidence packet (local, on the auditing machine):
~/.claude/plugins/data/plugin-quality-melodic-software/evidence/62c98632-.../repo-hygiene-clean/20260824T195700Z/
(evidence.md, audit-notes.md 37 KB, contract.md; seal verified changed=0 missing=0 unsealed=0)
HIGH
D1 — The destructive guard does not cover the operations the frontmatter says it covers
SKILL.md:7-11 states the mutating scripts "stay behind the PreToolUse destructive guard and the
permission flow". scripts/destructive-guard.sh:41-55 (is_destructive) matches only: rm with
both recursive and force flags; git clean with a force flag; git reset --hard;
git checkout -- ; git stash drop|clear; and Remove-Item …-Recurse.
Driven with synthetic PreToolUse payloads:
| Command |
Guard exit |
rm -rf /tmp/x |
2 (blocked) |
bash …/git-tree-reset.sh --apply |
0 (allowed) |
bash …/clean-batch.sh --tier all --apply --batch-plan /tmp/p |
0 (allowed) |
bash …/remove-path.sh --apply /d/repos/foo |
0 (allowed) |
git branch -D feature/x |
0 (allowed) |
git push origin --delete feature/x |
0 (allowed) |
git worktree remove --force /d/worktrees/x |
0 (allowed) |
None of the six mutating scripts the frontmatter names is gated. Neither is branch deletion — the
operation SKILL.md:142 and context/git-branch-cleanup.md §4.7 own end to end.
git push --delete is the only irreversible step in that path and the guard is blind to it.
The guard's own header honestly declares itself "a best-effort NET, not a security boundary" with
"known coverage gaps … accepted" — that posture is defensible. The defect is the frontmatter
claiming otherwise, plus the specific gap around branch and remote deletion, which is the skill's
own documented happy path rather than an obfuscation-class bypass.
D2 — matcher: "Bash" while the guard's regex targets PowerShell spellings
SKILL.md:19 declares matcher: "Bash". destructive-guard.sh:54 matches
Remove-Item[[:space:]].*-Recurse — proving the author intended PowerShell coverage. PowerShell is
a separate canonical tool, so Remove-Item -Recurse -Force issued through it never reaches the
hook. Not hypothetical: the audited machine has a first-class PowerShell tool and names PowerShell
as its primary shell.
The hooks documentation gives the remedy verbatim: the matcher Bash|PowerShell covers the
PowerShell tool as well as Bash. One-token fix.
D3 — gh pr list --limit 200 silently truncates the PR map, defeating the documented squash-merge mitigation
scripts/git-branch-audit.sh:53:
done < <(gh pr list --state all --json headRefName,state,number,headRefOid --limit 200 2>/dev/null |
context/git-branch-cleanup.md ("Squash-merge handling") correctly identifies that
git branch --merged misses squash merges and names gh pr list as the mitigation. That
mitigation is capped at the 200 most recent PRs across all states, and the truncation is never
reported — no count, no warning, no PRLimitHit: line. Past 200 PRs an older squash-merged branch
loses its PR row and lands in LIKELY-SAFE or REVIEW.
Scale context from the audited fleet: 416 branches carried a merged PR, and
claude-code-plugins alone has 1943 PRs. The operator hit exactly this bug class independently
with their own 1000-PR cap: re-fetching uncapped moved 187 branches from "needs a decision" to
"safe". Squarely in range.
Both gh and jq are also suppressed with 2>/dev/null on that line, so an unauthenticated or
missing gh yields an empty map indistinguishable from "no PRs exist" (see D8).
MEDIUM and below
- D4 [MEDIUM-HIGH] —
SKILL.md:110 carries an unconditional
REPO_ROOT=$(git rev-parse --show-toplevel). In a non-repo cwd this exits 128 on the
documented Action: menu path. Verified: the resolver itself always exits 0; the 128 is this
line. Guard it (2>/dev/null || true) and move it into the tiers that need a repo root.
- D5 [MEDIUM] —
clean-batch.sh never calls preflight.sh, though SKILL.md §1.5 and
context/action-router.md both promise it for the caches/build/all tiers. Three documents
promise a step no code runs. Call it once before the batch loop (not per repo), or remove the
promise.
- D6 [MEDIUM] —
CLEAN_GUARD_ACK=1 is documented as the recovery path with no mention of the
host permission layer above it. On the audited machine the auto-mode classifier independently
denied Stop-Process, rm -rf, a batched worktree unlock/remove script, and
git stash drop — so the documented recovery silently does not complete. The ack prefix also
breaks prefix-anchored allow rules.
- D7 [MEDIUM] — the five
allowed-tools grants never match the path they were written for.
- D8 [LOW-MEDIUM] — silent degradation of the
gh path (see D3's 2>/dev/null).
- D9 [LOW] —
reference/ecosystems.md is unreachable from the hub.
Gaps (missing capability, not defects)
- G1 [MEDIUM] — no lossy-but-deletable verdict state, and no guaranteed pre-deletion tip
capture. During the audited run an investigating agent returned 81 branches as DROP and flagged
5 of them, in prose rather than in the verdict column, as carrying work that had landed
nowhere. The deletion pass read the column and removed all five from local and origin. They
were recovered only because branch tip SHAs happened to have been captured beforehand. A third
state (drop-but-lossy), surfaced as its own block before the deletion confirmation, plus an
emitted Tip: <sha> per branch, makes that reversible by construction rather than by luck.
- G2 [MEDIUM] — no remote-branch analysis. The
git tier is remote-tracking prune only;
nothing inspects or deletes branches on origin. "Clean up feature branches, local and remote" is
not servable today.
- G3 [MEDIUM] — branch audit is cwd-only (no
--repo) and excluded from every batch tier.
- G4 [MEDIUM] — the WORKTREE tier routes nowhere: it correctly refuses to offer worktree-held
branches and says to use the worktree tool, but nothing hands off. 49 of 830 branches were
blocked behind this; all 29 merged-PR worktrees had to be found by hand.
- G5 [LOW-MEDIUM] — no
scan tier in the batch form.
- G6 [LOW-MEDIUM] — no progress output and no parallelism in
clean-batch.sh (the audited
dry-run ran ~40 minutes with no indication of progress).
- G7 [LOW] — stash dedup across a fleet is manual.
- G8 [LOW] — no canonical-clone realignment. 3 of 15 canonical clones were off their default
branch, which also makes those branches permanently undeletable by the audit.
- G9 [LOW] — per-branch spawn design (see the environment note below).
Environment factors — explicitly NOT charged to the plugin
Measured on the audited machine, recorded so the plugin is not blamed for them:
| Measurement |
Before Defender exclusions |
After |
/bin/true ×100 (Git Bash) |
323 ms/spawn |
235 ms/spawn |
git rev-parse ×100 |
366 ms/spawn |
301 ms/spawn |
hostname.exe ×100 (PowerShell, native) |
204 ms/spawn |
— |
85% of wall time was wait, not CPU. Causes were Windows Defender with zero exclusions and a
user statusline at refreshInterval: 2 against a 3.2-second render. Neither is the plugin's
fault. The plugin-owned half is only G9: git-branch-audit.sh is 187 lines with 14 git call sites
spawning per branch, so an environment multiplier lands on a per-item design. The 17-store branch
audit exceeded a 10-minute budget on its first attempt.
Correct and well-engineered behaviors (recorded for balance)
- The apply-manifest staleness guard worked: a worktree clean at survey time had gained 44
uncommitted files by apply and was correctly skipped, not removed.
remove-path.sh correctly refused a directory containing nested git repos.
git worktree remove correctly refused branches held by worktrees with live work (3 refusals,
all correct).
- The batch plan genuinely functioned as the gated set across a ~40-minute dry-run→apply gap.
- Dry-run-first was never bypassed by the scripts themselves.
- The dry-run/apply byte summaries (
planned=1800702976 vs removed=…42665984) were correct —
1673 MiB of the total lived in one worktree removed between the two phases — but the two numbers
invite a "the sweep failed" misreading. Worth a note in the apply output.
Blindspots — what this audit does NOT cover
One fleet run of the selective tiers only. Never exercised: tree / tree-batch (the
destructive tier), standalone scan.sh, and the --include-deps / --include-secrets /
--allow-unpushed flags. Findings touching those paths are source-reading only, not behavioral.
Suggested order (cheapest → most ambitious)
matcher: "Bash|PowerShell" (D2) — one token, closes a silent bypass
- Guard
SKILL.md:110's git rev-parse (D4)
- Correct the frontmatter's guard claim (D1)
- Raise the PR limit + emit
PRCount: / PRDataTruncated: / PRDataUnavailable: (D3, D8)
- Progress counter in the batch loop (G6)
- Emit
Tip: <sha> per branch and require tip capture before any deletion batch (G1)
- Document the host permission layer above the ack (D6)
- Preflight in the batch path, or drop the promise (D5)
- Third
drop-but-lossy verdict state (G1)
--repo flag + batched branch audit, collapsing per-branch git calls (G3 + G9)
- Remote-branch analysis (G2) — largest capability gap; land it after 6 and 9 since its bugs are irreversible
- Canonical-clone realignment tier (G8)
- Route the WORKTREE tier to an installed worktree surface (G4)
🤖 Generated with Claude Code
https://claude.ai/code/session_01TUApY97FHg7Ws7wEgGeCfZ
Post-use behavioral audit of
repo-hygiene:clean0.10.11, produced byplugin-quality:auditafter driving the skill through a full fleet cleanup: 65 working trees, 17 object stores,
14 GitHub repos, 830 branches, on Windows 11 / Git Bash + PowerShell 7.6.5.
Findings were produced by a fresh-context auditor reading the installed source, then the three HIGH
findings were independently re-verified against source by the dispatching session before filing.
Sealed evidence packet (local, on the auditing machine):
~/.claude/plugins/data/plugin-quality-melodic-software/evidence/62c98632-.../repo-hygiene-clean/20260824T195700Z/(
evidence.md,audit-notes.md37 KB,contract.md; seal verifiedchanged=0 missing=0 unsealed=0)HIGH
D1 — The destructive guard does not cover the operations the frontmatter says it covers
SKILL.md:7-11states the mutating scripts "stay behind the PreToolUse destructive guard and thepermission flow".
scripts/destructive-guard.sh:41-55(is_destructive) matches only:rmwithboth recursive and force flags;
git cleanwith a force flag;git reset --hard;git checkout --;git stash drop|clear; andRemove-Item …-Recurse.Driven with synthetic PreToolUse payloads:
rm -rf /tmp/xbash …/git-tree-reset.sh --applybash …/clean-batch.sh --tier all --apply --batch-plan /tmp/pbash …/remove-path.sh --apply /d/repos/foogit branch -D feature/xgit push origin --delete feature/xgit worktree remove --force /d/worktrees/xNone of the six mutating scripts the frontmatter names is gated. Neither is branch deletion — the
operation
SKILL.md:142andcontext/git-branch-cleanup.md§4.7 own end to end.git push --deleteis the only irreversible step in that path and the guard is blind to it.The guard's own header honestly declares itself "a best-effort NET, not a security boundary" with
"known coverage gaps … accepted" — that posture is defensible. The defect is the frontmatter
claiming otherwise, plus the specific gap around branch and remote deletion, which is the skill's
own documented happy path rather than an obfuscation-class bypass.
D2 —
matcher: "Bash"while the guard's regex targets PowerShell spellingsSKILL.md:19declaresmatcher: "Bash".destructive-guard.sh:54matchesRemove-Item[[:space:]].*-Recurse— proving the author intended PowerShell coverage. PowerShell isa separate canonical tool, so
Remove-Item -Recurse -Forceissued through it never reaches thehook. Not hypothetical: the audited machine has a first-class PowerShell tool and names PowerShell
as its primary shell.
The hooks documentation gives the remedy verbatim: the matcher
Bash|PowerShellcovers thePowerShell tool as well as Bash. One-token fix.
D3 —
gh pr list --limit 200silently truncates the PR map, defeating the documented squash-merge mitigationscripts/git-branch-audit.sh:53:context/git-branch-cleanup.md("Squash-merge handling") correctly identifies thatgit branch --mergedmisses squash merges and namesgh pr listas the mitigation. Thatmitigation is capped at the 200 most recent PRs across all states, and the truncation is never
reported — no count, no warning, no
PRLimitHit:line. Past 200 PRs an older squash-merged branchloses its PR row and lands in LIKELY-SAFE or REVIEW.
Scale context from the audited fleet: 416 branches carried a merged PR, and
claude-code-pluginsalone has 1943 PRs. The operator hit exactly this bug class independentlywith their own 1000-PR cap: re-fetching uncapped moved 187 branches from "needs a decision" to
"safe". Squarely in range.
Both
ghandjqare also suppressed with2>/dev/nullon that line, so an unauthenticated ormissing
ghyields an empty map indistinguishable from "no PRs exist" (see D8).MEDIUM and below
SKILL.md:110carries an unconditionalREPO_ROOT=$(git rev-parse --show-toplevel). In a non-repo cwd this exits 128 on thedocumented
Action: menupath. Verified: the resolver itself always exits 0; the 128 is thisline. Guard it (
2>/dev/null || true) and move it into the tiers that need a repo root.clean-batch.shnever callspreflight.sh, thoughSKILL.md§1.5 andcontext/action-router.mdboth promise it for the caches/build/all tiers. Three documentspromise a step no code runs. Call it once before the batch loop (not per repo), or remove the
promise.
CLEAN_GUARD_ACK=1is documented as the recovery path with no mention of thehost permission layer above it. On the audited machine the auto-mode classifier independently
denied
Stop-Process,rm -rf, a batchedworktree unlock/removescript, andgit stash drop— so the documented recovery silently does not complete. The ack prefix alsobreaks prefix-anchored allow rules.
allowed-toolsgrants never match the path they were written for.ghpath (see D3's2>/dev/null).reference/ecosystems.mdis unreachable from the hub.Gaps (missing capability, not defects)
capture. During the audited run an investigating agent returned 81 branches as
DROPand flagged5 of them, in prose rather than in the verdict column, as carrying work that had landed
nowhere. The deletion pass read the column and removed all five from local and origin. They
were recovered only because branch tip SHAs happened to have been captured beforehand. A third
state (
drop-but-lossy), surfaced as its own block before the deletion confirmation, plus anemitted
Tip: <sha>per branch, makes that reversible by construction rather than by luck.gittier is remote-tracking prune only;nothing inspects or deletes branches on origin. "Clean up feature branches, local and remote" is
not servable today.
--repo) and excluded from every batch tier.branches and says to use the worktree tool, but nothing hands off. 49 of 830 branches were
blocked behind this; all 29 merged-PR worktrees had to be found by hand.
scantier in the batch form.clean-batch.sh(the auditeddry-run ran ~40 minutes with no indication of progress).
branch, which also makes those branches permanently undeletable by the audit.
Environment factors — explicitly NOT charged to the plugin
Measured on the audited machine, recorded so the plugin is not blamed for them:
/bin/true×100 (Git Bash)git rev-parse×100hostname.exe×100 (PowerShell, native)85% of wall time was wait, not CPU. Causes were Windows Defender with zero exclusions and a
user statusline at
refreshInterval: 2against a 3.2-second render. Neither is the plugin'sfault. The plugin-owned half is only G9:
git-branch-audit.shis 187 lines with 14 git call sitesspawning per branch, so an environment multiplier lands on a per-item design. The 17-store branch
audit exceeded a 10-minute budget on its first attempt.
Correct and well-engineered behaviors (recorded for balance)
uncommitted files by apply and was correctly skipped, not removed.
remove-path.shcorrectly refused a directory containing nested git repos.git worktree removecorrectly refused branches held by worktrees with live work (3 refusals,all correct).
planned=1800702976vsremoved=…42665984) were correct —1673 MiB of the total lived in one worktree removed between the two phases — but the two numbers
invite a "the sweep failed" misreading. Worth a note in the apply output.
Blindspots — what this audit does NOT cover
One fleet run of the selective tiers only. Never exercised:
tree/tree-batch(thedestructive tier), standalone
scan.sh, and the--include-deps/--include-secrets/--allow-unpushedflags. Findings touching those paths are source-reading only, not behavioral.Suggested order (cheapest → most ambitious)
matcher: "Bash|PowerShell"(D2) — one token, closes a silent bypassSKILL.md:110'sgit rev-parse(D4)PRCount:/PRDataTruncated:/PRDataUnavailable:(D3, D8)Tip: <sha>per branch and require tip capture before any deletion batch (G1)drop-but-lossyverdict state (G1)--repoflag + batched branch audit, collapsing per-branch git calls (G3 + G9)🤖 Generated with Claude Code
https://claude.ai/code/session_01TUApY97FHg7Ws7wEgGeCfZ