Skip to content

disk-hygiene:clean: frontmatter Bash belt is session-lifetime, not skill-scoped (+6 findings from a post-use audit) #2618

Description

@kyle-sexton

TITLE

disk-hygiene:clean: frontmatter Bash belt is session-lifetime, not skill-scoped (+6 findings from a post-use audit)

BODY

A post-use audit of the disk-hygiene:clean skill turned up seven findings, one of them a false
statement of security posture with real blast radius: the skill's frontmatter PreToolUse belt is
documented in three places as scoped to "while the clean skill is active", but Claude Code registers
frontmatter hooks for the rest of the session, so its deny-by-default Bash lane stays armed long
after cleanup ends. The remaining six range from a scan boundary-condition bug to stale Gotchas that
contradict the shipped frontmatter. Every finding below was re-verified against 0.17.9 source by a
reviewer who was not in the run.

Provenance. Plugin disk-hygiene v0.17.9, skill clean, marketplace install. Real attended run
on a Windows 11 Dev Drive (D:\, non-OS volume root, 87 GB used / 937 GB free): two invocations
(audit-only, then --execute), 13 scan calls, 11 single-path handoff-verify calls, six removal
rounds, one permanent long-path deletion. Windows manual-handoff lane only — the engine apply path
never executed. Findings were produced by a fresh-context auditor reading the packet without the run
transcript, then independently re-verified against source before filing.

Source citations are repo-relative to plugins/disk-hygiene/.


What worked

Recorded first and with citations, because five mechanisms held under real pressure and a reader
skipping to the findings would get a false picture of this component.

  • The exact-tier-and-list confirmation gate held — skills/clean/SKILL.md:84-111. One surface
    rule, one floor, and a per-question table of what an answer must name, plus the rule that a bar
    may only require naming something the skill already showed. The run produced three bare "Approved"
    replies; two were resolved by asking for the naming, and for the single irreversible action the
    gate was held firm until a named confirmation arrived. A yes/no gate could not have drawn that
    distinction — the naming bar is what made it expressible.
  • Per-path handoff-verify before each deletion — skills/clean/SKILL.md:305-310. The staleness
    argument is given in full (the first path's check ages while later paths are still being walked,
    and staler after each intervening deletion), and the multi-path form is explicitly reserved for
    reporting. Observed: 11 single-path runs for 11 deletions, zero multi-path verdicts used for
    execution. The subcommand restates the rule in its own note field on every call — in-band
    reinforcement that does not depend on the model remembering the skill body.
  • Managed-state routing held against 29 GB — skills/clean/SKILL.md:55-57. Managed state is
    categorically ineligible "even when a native dry-run calls it eligible", which is strong precisely
    because it forecloses the most tempting argument. Observed: D:\packages (29.29 GB across nine
    package managers), D:\.pnpm-store, and D:\repos were routed and never planned; D:\Users was
    withheld from a tier while it looked chezmoi-owned and reclassified only on evidence.
  • truncated_paths coverage-gap reporting — skills/clean/scripts/hygiene.py:964-975 records
    every unwalked subtree with its reason, and preview independently refuses to act on them
    (truncated-not-inventoried, :1662-1663). Report-level honesty backed by an enforcement-level
    refusal rather than relying on the model to self-report. F2 below is a boundary-condition bug
    inside a mechanism that is otherwise the right design.
  • Reclaimable-vs-logical byte discipline — skills/clean/scripts/hygiene.py:660-679 explains why
    a qualified size can never be read as reclaimable and why not-walked yields null rather than
    0 ("collapsing 'not walked' into zero made a 1.35 GB truncated .cache indistinguishable from an
    empty folder"); SKILL.md:203-212 forbids summing logical_size by hand. Observed: D:\spike
    reported 78,445,515 logical against 72,824,131 reclaimable, preventing a ~5.6 MB overclaim.

Two smaller ones worth keeping: --execute is correctly unified as flag-not-approval across both
lanes (SKILL.md:39-43) and the first invocation offered no removal lane at all; and the Bash
denial text discloses the absolute interpreter and the authorized --data-root inline
(scripts/destructive_guard.py:1067-1089), so the documented bootstrap worked first try. No
prompt-injection surface was found anywhere in the skill body, reference, policy, or guard sources.


F5 — Session-lifetime deny-by-default Bash belt escapes the scope its own docs claim (High)

What it is. Three places in the plugin's own source state that the frontmatter belt is scoped to
active cleanup:

  • skills/clean/reference/safety-model.md:217-218 — the belt's lane is enforced "only while the
    clean skill is active
    " (contrasted with the engine gate at :214-215, which correctly says
    "whether or not the clean skill is active").
  • skills/clean/scripts/destructive_guard.py:644-646 — resolve_mode(): belt mode is "deny-by-default
    Bash and deletion-spelling PowerShell discipline, tolerable only while the clean skill is the
    active work
    ." The author's own stated tolerability precondition.
  • skills/clean/SKILL.md:362-363 — the belt "adds the deny-by-default Bash and deletion-spelling
    PowerShell discipline while cleanup is the active work."

The skills documentation's frontmatter field table says, verbatim (re-fetched from
code.claude.com/docs/en/skills.md during review, not recalled):

hooks — "Hooks that Claude Code registers when the skill is invoked and keeps running for the
rest of the session
."

There is no "skill is active" window at the harness level. The scope these three sites describe is not
one the platform provides.

Why the mistake is a natural one, and why that matters for the fix. The same page documents the
opposite lifetime for the other skill-frontmatter fields: an allowed-tools grant "clears when you
send your next message" (Skill content lifecycle), and disallowed-tools likewise — "the restriction
clears when you send your next message." So a skill-frontmatter permission really is scoped to
roughly the active-work window; a skill-frontmatter hook is not. The plugin is already deliberate
about the other frontmatter fields (SKILL.md:354-355 explains why the skill grants no
allowed-tools at all), so carrying the permission-field lifetime across to hooks is an easy slip
with a session-wide consequence — which is why fix 1 below is worth doing even if nothing else is.

Note the shape precisely: nothing here is stated backwards. The engine-gate clause at
safety-model.md:214-215 is correct. The belt clause asserts a narrower lifetime than the harness
gives it.

The spine — a correct principle applied to the wrong surface.
scripts/destructive_guard.py:426-430 shows the author reasoning correctly about exactly this hazard,
for the other surface:

"A plugin-level hook fires on every shell call in every session, so a bare mention of the engine's
filename ... must defer — routing mentions into the belt's fail-closed parser would block ordinary
work session-wide."

That defer-fast discipline was engineered into the gate and withheld from the belt, because the belt
was believed to be time-scoped. This is not an oversight about blast radius; it is a sound principle
applied to the wrong surface.

Blast radius. The belt's Bash lane is deny-by-default with an allowlist of exactly four bundled
engine subcommands plus one argument-free probe (scripts/destructive_guard.py:309;
:1067-1089). Once the skill is invoked, Bash calls on that session's main thread — by any other
plugin, any other skill, and the user — are denied for the remainder of the session unless they match
one of those five shapes.

The realized harm was not inconvenience. Observed in the run: a bare true was denied; a scan with
2>&1 | head -c 700 appended was denied; and a sibling plugin's bundled integrity tool
(plugin-quality's packet-seal.sh record) was denied with disk-hygiene's fail-closed message after
cleanup had already finished. That is cross-plugin denial-of-verification caused by a plugin the
operator invoked for an unrelated purpose — and it is why this audit's own evidence packet went
unsealed and its integrity ungraded.

Reproduction (command held constant, one variable).

Session clean invoked? packet-seal.sh on the same host/user
Main thread of the run yes denied, disk-hygiene fail-closed message
Fresh session, same script and host no permitted, record and verify both exit 0

Holding the command constant and varying only whether clean was invoked isolates the denial to belt
registration lifetime rather than anything about the script, the interpreter, or the host.

Scope qualifier (verified during review). Bash calls issued from subagent threads in the same
session were observed not to be subject to the belt — echo, ls, and bare engine-filename mentions
were all permitted from a subagent of the session that had invoked clean. This is stated as an
observation, not a mechanism claim. It does not reduce severity: the operator works on the main
thread, and that is where the realized harm occurred. It does confirm the reproduction above was a
valid control rather than a confound.

No escape hatch. Nothing in SKILL.md, safety-model.md, or destructive_guard.py documents a
way to disarm the belt once registered. A grep of the guard for DISK_HYGIENE_* returns exactly one
env var, DISK_HYGIENE_GUARD_WATCHDOG_SECONDS (scripts/destructive_guard.py:1092), which tunes the
watchdog and explicitly cannot disarm it (:1110-1119: an invalid override "falls back to the default
rather than disarming the watchdog"). disk_hygiene_enabled: false makes the guard stricter, not
looser. The only exit is ending the session. That is what turns a scope bug into an unrecoverable one.

Related, and worth folding into the same fix review. The gate's stated intent at :426-430 (bare
mentions must defer, or ordinary work is blocked session-wide) has a documented exception at
:459-461: a path-form word that resolves to the same file as the bundled engine gates regardless of
invocation shape
, and :455-457 then hands it to the belt's rules. Verified live on a clean session
with clean never invoked: echo "... hygiene.py" and a relative-path mention both deferred, while
wc -c "<abs-path>/skills/clean/scripts/hygiene.py" — a pure read — was denied with the
deny-by-default message. So a narrow slice of belt deny-by-default is live in every session on
every install. This looks deliberate (it closes the symlink/hard-link alias-evasion class the
docstring describes) and the denial text names the correct workaround ("Use non-Bash read-only tools
for supporting inspection"), so it is raised as a documented-tradeoff note rather than a defect — but
:426-430's stated intent and :459-461's implemented rule are in tension and one of them should be
reworded.

Impact. For the remainder of any session in which clean is invoked, unrelated tooling on the
main thread cannot run, with no documented recovery short of ending the session.

Recommended fix, cheapest first.

  1. Correct the three documentation sites (safety-model.md:217-218, resolve_mode() docstring at
    destructive_guard.py:644-646, SKILL.md:362-363) to state the real lifetime. Zero behavior
    change; removes a false statement of security posture but leaves blast radius exactly as-is. Do
    this regardless of what else is chosen.
  2. Belt-mode Bash returns ask instead of deny for unrecognized commands. Keeps a human gate,
    removes the hard lockout. Honest costs: a permission prompt on every unrelated Bash call for the
    rest of the session, and a genuinely weaker fail-closed posture during active cleanup, which is
    the window the belt exists for.
  3. Recommended — real time-scoping via a run-directory sentinel. The skill already creates a
    unique run directory under ${CLAUDE_PLUGIN_DATA}/runs/ at step 1 (SKILL.md:115), and the guard
    already derives the data root from --plugin-root rather than the environment
    (_plugin_data_root_from_root, destructive_guard.py:219) — which matters here, because the
    guard's own environment lacks CLAUDE_PLUGIN_DATA (SKILL.md:128). Gate belt-mode
    deny-by-default on an open run marker — dropped at step 1, cleared at the step 6 summary — and
    fall back to deny when the marker state is unreadable. This delivers precisely the scope the docs
    already claim, using state the guard can read today, with no new channel and no weakening during
    active cleanup.
  4. Allowlisting other plugins' cache-resident scripts was considered and is argued against: a
    <plugins>/cache/*/**/*.sh allowance is an enormous hole in a deny-by-default belt, since any
    cached plugin script can do anything, including deletion. Strictly inferior to (3).
  5. once: true is wrong here — it unregisters the hook after its first run, gutting the belt
    mid-cleanup. Named so nobody reaches for it.

F2 — An empty directory can never be a removal candidate at its own scan depth (Medium)

What it is. skills/clean/scripts/hygiene.py:972-975:

elif max_depth is not None and depth >= max_depth:
    subtotal = None
    walked = False
    truncated.append(relative)

The depth test is evaluated before any os.scandir of the child, so it cannot consult whether
children exist. Every directory at depth == max_depth is marked walked=False regardless of
content. metadata() then records logical_size: None and appends the not-walked qualifier
(:685-691); the path lands in truncated_paths (:1037); and SKILL.md §1 instructs never to plan
those for removal, which preview enforces as truncated-not-inventoried via overlaps_truncated
(:1139-1151, :1662-1663).

Net: at a --max-depth N scan, no directory at depth N is ever a candidate, because emptiness is
unobservable at the boundary.

Evidence. Reproduced twice in the run. After D:\spike was emptied, a --max-depth 1 scan still
listed spike in truncated_paths; only --max-depth 2 cleared it. D:\spike and D:\Users each
required a delete-child → rescan-at-depth+1 → delete-parent cycle, two extra scan/verify rounds each.

Second, unreported consequence. The same branch nulls logical_size and so excludes the directory
from reclaimable totals. For an empty directory both effects are wrong in the same direction: it is
vacuously fully inventoried, and its size is genuinely 0, not unknown.

Impact. Two extra approval-gated scan/delete rounds per affected directory, and understated
inventory completeness at every scan boundary.

Recommended fix, cheapest first.

  1. At the depth boundary, probe for a first child with a single os.scandir / next(iter(...)) before
    marking walked=False and appending to truncated. If there is no child, record walked=True,
    logical_size=0, no not-walked qualifier, and do not add to truncated. Safety argument: an
    empty directory has no descendants, so nothing uninventoried remains for
    truncated-not-inventoried to protect — the preview block loses no coverage and the byte
    accounting becomes exact. One cheap scandir per boundary directory; fixes both effects.

    Two constraints for whoever applies this. (a) Scope the change to the depth branch at
    :972-975 only. The identical subtotal = None; walked = False; truncated.append(relative)
    shape also serves the VCS_NAMES branch (:964-967) and the protections branch (:968-971),
    where walked=False is correct and deliberate — an empty directory that is protected or is a VCS
    root must still land in truncated. Applying the probe generally ships a real regression.
    (b) truncated_paths has four downstream consumers, not one: the preview blocker (:1662-1663),
    the unverified path (:1691, :1698), apply's contested set (:1797, :1803), and snapshot
    passthrough (:2273). Clearing an empty boundary directory from truncated changes behavior at
    all four.

  2. Failing that, document the constraint in SKILL.md §1 so an operator plans a depth+1 scan
    deliberately rather than discovering the cycle empirically.

  3. Most ambitious: distinguish not-walked (depth cut) from empty-verified as separate qualifiers,
    so downstream consumers can tell the two apart explicitly.


F4 — "Prefer reversible removal" has no long-path branch (Medium)

What it is. skills/clean/SKILL.md:311-314 lists exactly three conditional-reversibility caveats
— "bin size caps, a policy-disabled bin, or a non-NTFS/network volume". A case-insensitive grep of
every .md under skills/clean/ for long path, MAX_PATH, 260, \\?\, and path length
returns no matches. The omission is total, not merely un-emphasized.

Why it matters more than a missing bullet. The three listed caveats all describe a reversible
operation silently becoming permanent. Path length is a different failure: the operation cannot be
attempted at all through the shell APIs behind the Recycle Bin, so the operator must derive an
alternative unaided — and the only alternative is a permanent \\?\ long-path delete. The step
that exists to steer toward reversibility hands the operator, without guidance, the single
irreversible action in the run.

Evidence. D:\Users\KyleSexton — 1,329-character path, 27 levels, 119 MB — was deleted
permanently via Directory.Delete through a \\?\ path, the only permanent deletion in six removal
rounds. The confirmation gate caught what step 6.2 did not warn about: the gate was held firm and a
named confirmation was required before proceeding.

Impact. An operator following step 6.2 literally reaches an irreversible action believing they are
inside the reversible lane, with the tier approval they were given for reversible removals.

Recommended fix, cheapest first.

  1. Add path length to the 6.2 caveat list, naming the consequence: the fallback is permanent, so it
    requires its own explicit approval as an irreversible action rather than inheriting the tier
    approval given for reversible removals.
  2. Have handoff-verify emit a per-path advisory when a resolved path exceeds the classic limit, so
    the warning is deterministic rather than model-recalled.
  3. Add a reversibility field to the handoff-verify verdict (reversible / permanent-only) with
    its reason, making the risk decision machine-legible and forcing re-approval in step 6.

F6 — Gotchas describe the belt's launch form as exec-form; the shipped frontmatter is shell form (Medium)

What it is. The frontmatter registers the belt in shell form —
skills/clean/SKILL.md:22-24 names "${CLAUDE_PLUGIN_ROOT}"/hooks/run-python-hook.sh ... with
shell: bash, and its own inline comment says so ("Shell form, matching hooks/hooks.json (#2568)").
skills/clean/reference/safety-model.md:196-197 agrees: the belt is "shell form through the same
launcher since 0.17.9" — the version shipped.

skills/clean/SKILL.md:377-390 still says the opposite:

"This skill's frontmatter belt still launches in exec form via python3, resolved on PATH
with no shell ... on a host where python3 does not resolve ... the PreToolUse launch fails, and
Claude Code treats a failed hook launch as a non-blocking error, so the belt does not intercept
there (converting this surface is tracked in #2568)."

The conversion shipped; the bullet did not move with it. The harness premise the bullet rests on is
itself correct — a failed hook launch is a non-blocking error — which is exactly what makes the
stale bullet plausible enough to be believed.

Why it is not cosmetic. The bullet tells an operator that on some hosts the belt silently does not
fire, that "defense-in-depth [is] lost, not preserved", and that /disk-hygiene:setup check should be
consulted. An operator reading current docs would either under-trust a guard that is in fact firing,
or reach for a bypass that no longer exists. It is a false statement of security posture in the
operator-facing document — in the safer direction, but false. This bullet and F5's sit ten lines
apart and are wrong in opposite directions: one under-claims enforcement that exists, the other
over-claims a scope limit that does not.

Mechanism, and why it will recur. The two-surface registration story, the kill-switch history, and
the PowerShell tradeoff are each maintained in full in both SKILL.md Gotchas (:360-411) and
reference/safety-model.md (:174-221, :258+). Hand-maintained duplication with no derivation:
safety-model.md was updated for #2568 and the SKILL.md copy was not. The drift is the predicted
consequence of the duplication, not an isolated slip.

Impact. Operators are told a guard may be silently inert when it is armed, and pointed at a
remedy for a condition that no longer exists.

Recommended fix, cheapest first.

  1. Correct the SKILL.md:377-390 bullet to describe shell-form launch. Do it alongside F5 fix 1 —
    same file, adjacent lines, one edit.
  2. Reduce the Gotchas duplication to one-line pointers into reference/safety-model.md, leaving that
    file as the single origin. Also relieves the progressive-disclosure pressure in F7.
  3. Add a test asserting the frontmatter's declared launch form matches what the docs describe. The
    plugin already has precedent for exactly this kind of cross-file assertion —
    test_declared_hook_timeouts_match_the_watchdog_ceiling, referenced at
    scripts/destructive_guard.py:1095-1100, fails the suite when the two registrations' timeouts
    drift.

F1 — Tier rubric has no misplacement axis, and the one ordering rule is undercut by the field list (Low-Medium)

What it is — two parts.

Confirmed: no misplacement axis. The §3 rubric (skills/clean/SKILL.md:193-197) grades
disposability evidence only across all three tiers — provenance strength, owner activity,
work-product resolution. There is no axis for content that is correctly identified but in the wrong
location, and no axis for tidiness value independent of bytes. Confirmed by grep: misplac and
relocat appear nowhere in SKILL.md.

Corrected during review — the ordering rule exists. The audit initially reported that no ranking or
salience instruction existed anywhere in SKILL.md. That is wrong, and the correction changes the
diagnosis. skills/clean/SKILL.md:191 reads:

"Confidence is report priority, not permission:"

So tier-drives-report-priority is stated. The real gap is that it is one compressed clause
introducing a table, easily read as "which findings deserve attention" rather than "presentation
order" — and the report field list two lines later at :199-201 leads with logical bytes ("Report
every finding with path, logical bytes, tier, evidence, ..."), pulling in the opposite direction.
The observed failure was therefore a report that violated an existing instruction, not one that
filled a guidance vacuum.

Two adjacent facts push the same way and are worth naming: §3's closing "Empty directories are not
inherently junk" (:201) argues against flagging exactly the zero-byte-chain case that prompted this
finding, and the Low tier's own text is "Keep unless the human separately reviews and approves
exact paths" — the rubric de-prioritized removal, it did not forbid it.

Evidence. In the run, a zero-byte empty directory chain created by a mis-resolved path was
reported in a way that buried it; the report ranked by bytes and closed with "not worth the risk", and
the user's response was to point at six root entries and ask why they had not been flagged. They had
been — the ordering buried them.

Impact. Correctly-identified findings are presented so that a tidiness-motivated operator cannot
find them, on a volume where byte size is not the operator's concern.

Recommended fix, cheapest first.

  1. Sharpen :191 from "Confidence is report priority" to an explicit ordering instruction (report
    ordered by tier and evidence strength; byte size never determines presentation order), and reorder
    the :199 field list so tier precedes logical bytes. This is a two-line edit that addresses
    the actual cause. Note it supersedes the audit's original suggestion to add an ordering rule —
    the rule is already there and needs strengthening, not duplicating.
  2. Add a non-tier annotation (e.g. tidiness-only, misplaced) that is reportable but confers no
    removal permission, so zero-byte residue is legible without inflating its disposability tier.
  3. Add a genuine second axis (disposability × placement) to the rubric with its own default-outcome
    column. Most faithful to the real gap and the largest change to a safety-critical table — the axis
    must not become a removal permission by itself.

F3 — No relocation lane, and Move-Item / Rename-Item are unguarded (Low-Medium)

What it is. SKILL.md models exactly two outcomes for any finding: approve exact paths for
deletion (§4-§6), or keep (§3). Grep confirms no move/relocate concept anywhere in the skill body. The
engine's subcommands are scan, preview, handoff-verify, apply
(scripts/destructive_guard.py:309) — no relocation primitive exists, so there is nothing to expose.
Designed-in, not an oversight.

The aggravating factor. skills/clean/reference/safety-model.md:181-189 states that the PowerShell
belt's flagged set is "deletion- and recycle-shaped (plus robocopy mirror/purge/move and .NET
Delete)", so destructive non-deletion spellings — explicitly Move-Item/mv, Rename-Item,
overwriting writers, and volume operations — "reach the tool with no guard verdict at all, in
audit-only mode included". TODO(#387) at :191 tracks the gap. The same passage names the specific
hazard at :188: a Move-Item -Force collaterally destroys whatever it overwrites at the
destination, and the manual handoff's per-path approval does not reach that.

So the relocation that happened in the run — a 76.2 KB file of unique research moved out of D:\tmp
to a working directory, performed outside the skill because the skill has no lane for it — was not
merely unrecorded and unverified. It was the one mutation in the entire run that passed with no
guard coverage of any kind
.

Impact. The workflow silently implies completeness it does not have, and the mutation it pushes
outside itself is the one its own guard does not cover.

Recommended fix, cheapest first.

  1. State the boundary explicitly in §3: relocation is out of scope, the skill offers keep-or-delete
    only, and a move must be performed and verified by the operator outside the workflow. Cheapest, and
    it stops the workflow from implying completeness.
  2. Add a relocate disposition that is report-only — names the path, a suggested destination, and
    the rationale in the §3 report, with the engine performing nothing. Preserves the record and the
    audit trail at no new mutation surface.
  3. Close TODO(#387) so Move-Item/Rename-Item at least reach the same final-human-prompt bar as
    deletion spellings. Independent of whether a relocation lane is ever built, and arguably the
    higher-value half of this finding.
  4. Most ambitious: a real relocation lane with snapshot, destination collision check, a
    handoff-verify equivalent, and post-move verification. Largest new mutation surface in a plugin
    whose entire premise is that mutation is expensive to make safe — recommend against unless demand
    is demonstrated.

F7 — Hub is not thin; progressive disclosure inverted (Low)

What it is. skills/clean/SKILL.md is 29,617 bytes / 414 lines, of which the Gotchas section
(:338-414) alone is 76 lines of harness minutiae — while reference/safety-model.md (28,323 bytes,
356 lines) exists and is linked once, at SKILL.md:35. Per the skills documentation's content
lifecycle, a rendered SKILL.md enters the conversation as a single message and stays there for the
rest of the session, so hub weight is paid in full on every invocation including audit-only runs,
while the reference is loaded on demand. The heavy material is on the wrong side of that boundary.

Impact. Every invocation, including read-only audits, pays for ~76 lines of hook-launch and
kill-switch history that the run never needs.

Recommended fix. Move the harness-mechanics Gotchas into reference/safety-model.md — already
their home, and already carrying the same content — leaving load-when pointers behind. This is the
same edit as F6 fix 2; doing it once discharges both.

Not a finding, recorded as an observation. The description is 601 characters
(skills/clean/SKILL.md:2). It is well-constructed for triggering — front-loaded use cases plus
explicit "Skip when" boundaries — and no documented hard cap was found to cite against it.


Limitations of this audit

Stated so nothing above is read as broader coverage than it has.

  • No Linux apply lane evidence — the only lane where the engine itself deletes. This run was
    Windows-only; every removal went through the manual handoff, and preview/apply were never
    reachable. O_NOFOLLOW descriptor-relative unlink, device/inode identity matching, lsof
    fail-closed behavior, and /proc/self/mountinfo parsing are all asserted in the Gotchas and none
    were exercised. The most safety-critical code in the plugin has zero behavioral evidence here.
  • skill-quality:check could not run. check-skill.sh and check-listing-budget.sh (v0.15.9)
    both exit 2 with Error: not in a git repo when pointed at a plugin-cache install. The audit's
    REQUIRED static gate for a skill component is therefore unsatisfied, and F7's hub-thinness
    assessment is a manual walk rather than a tool result. This is a portability observation about
    skill-quality, not a disk-hygiene defect — both plugins live in this repo, so please do not
    act on it as one.
  • Evidence-packet integrity is ungraded. packet-seal.sh verify returned exit 2 (nothing sealed)
    at audit start: the run's evidence file was written before any seal existed, so the seal now
    covering it protects forward divergence only. Any pre-audit alteration would be undetectable. This
    is a direct downstream effect of F5 — the sealing tool was one of the things the belt denied.

Also unexercised, listed briefly: macOS Trash path; audit-only mode (disk_hygiene_enabled: false
was never in force — the probe reported {"effective": true, "source": "default"}), so the
managed-settings-precedence claim at safety-model.md:207-211 is empirically unverified; policy
overlay layering and --policy replacement semantics; the Stop guard_launch_monitor detector,
which never fired; multi-path handoff-verify staleness (reasoned about, never observed); non-NTFS,
network, and bin-capped volumes — meaning the three reversibility caveats step 6.2 does list are
themselves unexercised, adjacent to F4; --plugin-dir checkout installs; and the belt's PowerShell
lane, which was never observed blocking, so its flagged-spelling coverage is unverified in practice.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageNot yet classified. Floor until a type and one priority tier are set.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions