fix(disk-hygiene): surface silent destructive-guard launch failures - #1465
Conversation
…1416) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e2ff216f3a
ℹ️ 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".
|
🤖 Autonomous merge lane — threads validated, deliberately left unresolved. (AI-generated.) The lane was authorized to clear review threads so this PR could merge. It did not, because both findings are VALID and neither fix is one an autonomous lane should pick. Nothing was pushed; head is unchanged at P1 — shared-interpreter blind spot (
|
|
Claude finished @kyle-sexton's task in 2m 4s —— View job PR Review: fix(disk-hygiene): surface silent destructive-guard launch failures
SummaryThe implementation is solid and the design decisions are well-reasoned. The stdlib-only detector, deliberate fail-open posture, bounded tail read, and once-per-session marker are all sound. Test coverage is thorough (17 scenarios). Two findings require attention before this merges. FindingsP1 — Shared-interpreter blind spot (confirms the open thread)File: The The autonomous lane's assessment is correct — the remedy (shell shim, absolute-path resolution mirroring what The cheap, PR-scoped fix that IS worth doing: The three "does not cover" disclosures in P2 — Telemetry convention gap (confirms the open thread; not this PR's to close)File: The gap is real: Implementation notes (no action required)
VerdictRequest changes on P1's documentation gap; defer P2 to a separate tracker. The implementation is correct and the test suite is comprehensive. The one fix that belongs in this PR is adding the shared-interpreter exclusion to the three "does not cover" lists — it's a doc-only change that takes five minutes and makes the stated coverage match the actual code. The launcher design question (how to break the interpreter coupling) should be tracked separately. |
|
Claude finished @kyle-sexton's task in 3m 17s —— View job Security Review: fix(disk-hygiene): surface silent destructive-guard launch failures
FindingsIMPORTANT · Confirmed — Detector and guard share the same interpreter; detector is blind exactly when neededBoth hooks in The plugin's own The three "does not cover" disclosures added to SUGGESTION · Plausible — Arbitrary file read via stdin-supplied
|
The three coverage-disclosure sites enumerated three exclusions and omitted the one that matters most: both wired hooks declare the same literal `python3` command, so the interpreter-resolution fail-open the README documents most prominently takes the detector down with the guard it watches. The documented envelope read wider than the code delivers. Closing the gap itself needs a launcher whose availability is independent of the guarded interpreter -- a design decision this change does not settle, tracked in #1504.
|
Claude finished @kyle-sexton's task in 2m 15s —— View job PR Review: fix(disk-hygiene): surface silent destructive-guard launch failures
SummaryTwo commits under review:
The implementation is solid and the test suite is thorough. The new commit resolves the one PR-scoped fix that was blocking this PR. Both open threads are still unresolved by design — this review confirms the correct disposition for each. P1 — Documentation gap: RESOLVED by
|
|
Claude finished @kyle-sexton's task in 4m 2s —— View job Security Review: fix(disk-hygiene): surface silent destructive-guard launch failures
OverviewReviewed the 8 changed files. The PR adds a new Prior reviews identified two concerns; I independently verified both and add one new finding. Results below, severity- and confidence-tagged. Finding 1 — IMPORTANT · Confirmed — Shared interpreter makes the detector blind on its highest-priority fail-open vectorConfidence: HIGH — directly readable in two places; no inference required. Files: Both hooks declare the identical literal // PreToolUse guard (line 9)
"command": "python3"
// Stop detector (line 28)
"command": "python3"When Status as of HEAD (7209392): The disclosure gap (prior reviewer's "cheap fix") is now addressed — Finding 2 — SUGGESTION · Confirmed — Arbitrary file read via stdin-supplied
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7209392dab
ℹ️ 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".
The bounded tail read discarded its first line unconditionally. When `size - _MAX_TAIL_BYTES` lands exactly on a record's first byte the window already starts on a boundary, so the discard threw away a whole record -- which can be the session's only guard failure, silencing the report the detector exists to make. The read now probes the byte before the window and discards only when the offset is genuinely mid-record. Covered by a boundary fixture that places the sole failure record at the window's first byte; it fails against the previous unconditional discard.
|
Claude finished @kyle-sexton's task in 2m 43s —— View job PR Review: fix(disk-hygiene): surface silent destructive-guard launch failures
New commit:
|
| Finding | Status |
|---|---|
| Codex P2 (tail boundary — discard when already on boundary) | Fixed by a3976c6 |
| claude-review P1 (documentation gap — shared-interpreter exclusion missing) | Fixed by 7209392 |
| Codex P1 / claude-review (shared-interpreter blind spot — code level) | Open, correctly deferred to #1504; documented at all three coverage-disclosure sites |
| Codex P2 / claude-review (telemetry convention gap) | Open, correctly deferred to #1505 |
Security Finding 2 (arbitrary file read via transcript_path) |
Acknowledged, low severity, no action |
Security Finding 3 (guard stderr in systemMessage) |
Intentional design trade-off, no action |
Security Finding 4 (str(session_id) on non-string JSON value) |
Still present, SUGGESTION/LOW confidence, acceptable |
Verdict
Approved. The new commit correctly fixes the tail boundary bug, the test validates the exact byte-level scenario, and the CHANGELOG entry is updated. All open threads are at their correct dispositions — the two code-level deferred items (#1504, #1505) have live trackers and are not regressions introduced by this PR. Nothing remaining here warrants holding.
| Branch
|
Claude finished @kyle-sexton's task in 2m 59s —— View job Security Review: fix(disk-hygiene): surface silent destructive-guard launch failures
HEAD reviewed: Three previously-identified findings survive at current HEAD; one new edge-case observation on the latest commit. No injection, command-execution, or privilege-escalation surfaces found beyond the four items below. IMPORTANT · Confirmed — Detector shares the guard's interpreter; blind on the most prominent fail-open vectorFile: Both hooks declare the identical literal Status at HEAD: Code-level exposure remains open by design. Commit SUGGESTION · Plausible — Arbitrary file read via stdin-supplied
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a3976c65f9
ℹ️ 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".
…warning The once-per-session marker was written before the systemMessage was printed. A closed pipe, or termination between the two, left the marker in place with nothing delivered -- and the broad never-fail-loudly handler exited quietly -- so every later Stop in the session saw the marker and suppressed the warning permanently, reinstating the silence this detector exists to break. `_run` now returns the warning together with where to record it and leaves the recording to the caller, which marks only after `print` and an explicit `flush`. Covered by a broken-pipe fixture asserting the warning stays repeatable on the next invocation.
|
Claude finished @kyle-sexton's task in 1m 26s —— View job PR Review: fix(disk-hygiene): surface silent destructive-guard launch failures
New commit:
|
| Finding | Status |
|---|---|
| Codex P1 / shared-interpreter blind spot (code) | Open, correctly deferred to #1504; all three disclosure sites enumerate it |
| Codex P1 / documentation gap | Fixed by 7209392 |
| Codex P2 / tail boundary (unconditional readline) | Fixed by a3976c6 |
| Codex P2 / marker written before delivery | Fixed by abfbec9 |
| Telemetry convention gap (plugin-wide, pre-existing) | Correctly deferred to #1505 |
Security: transcript_path path restriction |
SUGGESTION/low; no blocker |
Security: guard stderr in systemMessage |
Intentional design; documented |
Security: str(session_id) on non-string JSON value |
SUGGESTION/low; no blocker |
Implementation notes (no action required)
The full implementation at HEAD is clean:
_run()→tuple[str, list[Path]] | None— the signature change is the right abstraction; it makes the "caller marks, not callee" contract explicit in the type rather than relying only on the docstring.sys.stdout.flush()before_write_marker()—flush()is the right flush point;print()alone offers no delivery guarantee.test_undelivered_warning_is_not_marked_as_warned— precise fixture that covers the exact broken-pipe scenario; the two-phase structure (run with broken pipe, then retry and assert content) is the minimal and sufficient test.CHANGELOG.mdentry updated to cover both the tail-boundary and marker-ordering fixes in0.9.5.
Verdict
Approved. The Codex marker-ordering finding is correctly and cleanly fixed by abfbec9. All three per-commit correctness findings (documentation gap, tail boundary, marker ordering) are resolved. The two open review threads are at their correct dispositions — shared-interpreter launcher design tracked in #1504, telemetry adoption tracked in #1505. The implementation is sound, the test suite is comprehensive, and nothing remaining here warrants holding.
| Branch
|
Claude finished @kyle-sexton's task in 2m 33s —— View job Security Review: fix(disk-hygiene): surface silent destructive-guard launch failures
HEAD reviewed: No injection surfaces, command execution vectors, credential exposure, or GitHub Actions issues found. Three findings carry over from prior reviews (two SUGGESTION, one IMPORTANT). One prior finding (marker-before-delivery) is confirmed fixed. Full accounting below. IMPORTANT · Confirmed — Shared interpreter makes the detector blind on its most prominent fail-open vectorConfidence: HIGH Files: Both hooks declare the identical literal Status at HEAD: Code-level exposure is open by design and disclosed at all three coverage sites ( SUGGESTION · Plausible — Arbitrary file read via stdin-supplied
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: abfbec9d22
ℹ️ 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".
#1465 landed the #1416 detector as disk-hygiene 0.9.5 on main while this branch also held 0.9.5 for the #1423 fail-open fix. Both entries are real and independent, so this branch's moves to its own 0.9.6 heading above main's 0.9.5 and the manifest matches, giving changelog-parity a real bump with a newly added entry. 209 guard tests and 19 detector tests pass on the merged tree.
) Fixes #1505 ## Summary - Add `plugins/disk-hygiene/lib/hook_telemetry.py` — stdlib-only native Python telemetry emitter mirroring `hook::emit_telemetry` (deliberate parallel path, not a bash subprocess wrapper, for Windows portability and the guard-launch monitor's low failure surface). - Wire `destructive_guard.py` (PreToolUse) to emit `ok` / `blocked` / `error` envelopes with privacy-safe `tool` + `decision` data; engine-gate inapplicability short-circuits stay silent. - Wire `guard_launch_monitor.py` (Stop) to emit `ok` when a transcript scan finds no guard failures and `error` with `failure_count` when it surfaces silent guard launch/runtime failures; pre-evaluation short-circuits stay silent. - Publish per-hook `data` schemas and bump plugin to 0.17.5. ## Test plan - [x] `python3 -m unittest plugins/disk-hygiene/lib/test_hook_telemetry.py` - [x] `python3 -m unittest plugins/disk-hygiene/skills/clean/scripts/test_guard_launch_monitor.py` - [x] `python3 -m unittest test_hygiene.GuardTests.test_deny_emits_blocked_telemetry_when_sink_wired test_hygiene.GuardTests.test_engine_gate_irrelevant_emits_no_telemetry` (from `skills/clean/scripts/`) ## Related - Closes the disk-hygiene adoption gap tracked from #836 / #1465 Codex review - Reuse-or-replace decision: native Python emitter in `disk-hygiene/lib/` establishes precedent for future Python hooks --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
…1504) Fixes #1504 ## Summary Both wired `disk-hygiene` hooks now register as `bash` invoking `hooks/run-python-hook.sh`, which resolves a real Python 3 interpreter (rejecting the zero-length WindowsApps `python3` alias stub) before exec'ing the target script. When no interpreter resolves, the guard still fails open (exit 0) and the Stop detector emits a `systemMessage` on stdout. ## Related - #1465 — disclosed the blind spot this change closes - #1416 — guard-launch monitor that benefits from independent launcher --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Closes #1416
Summary
2026-07-25T23:46-23:47Z) found both originally-reported launch-refusal root causes already fixed
and merged (disk-hygiene: fix(disk-hygiene): resolve kill switch by reading user settings directly #1242/0.9.0; repo-hygiene: fix(repo-hygiene): destructive-guard fail-open on Windows — shell-form hook launch + jq fail-closed degraded mode #1006), and split the one remaining live
defect (a silent post-launch death) to disk-hygiene: destructive guard exits 1 with empty stderr after 17s — a live fail-open distinct from #1242 #1423, fixed separately by fix(disk-hygiene): destructive guard denies at exit 2 instead of failing open at exit 1 #1449. What disk-hygiene: the destructive-operation guard has never launched — 73 recorded runs, all hook_non_blocking_error #1416 kept as its
own scope, per the operator's brief and its amendment: make a guard-launch/runtime failure loud,
because "the guard denied nothing because it approved" and "the guard denied nothing because it
never ran, or ran and died" were indistinguishable from outside the harness.
plugins/disk-hygiene/skills/clean/scripts/guard_launch_monitor.py. A second,independent hook — stdlib-only, imports nothing from
destructive_guard.pyorlib/— registeredon
Stop(notPreToolUse/PostToolUse) inhooks/hooks.json. Deliberately not per-tool-call:this repo already paid for that mistake once
(
docs/adr/0004-rightsize-instruction-surfaces-by-incumbent-first-arbitration.md's D-12, aguardrails
PreToolUsehook costing 12-19s p50 on every Bash call). Per thehooks reference (fetched 2026-07-25),
Stopfires onceper turn — the guard, if it ran, ran synchronously before the guarded command, so its failure
record is already in the transcript well before the turn ends. The read itself is a bounded
byte-seek tail (2MB cap) so per-turn cost never scales with session length, and a once-per-session
marker (keyed by the hook's own
session_idinput, never a field found inside transcriptrecords — those can differ from the file's own session, confirmed empirically against real local
transcripts) short-circuits the read entirely after the first warning.
systemMessagestates the most recentfailure's
exitCodeanddurationMsexplicitly (labelled, not just embedded) alongside truncatedstderr and the total failure count — verified both by the rendered-string test in
test_guard_launch_monitor.py(fixture shaped like the real disk-hygiene: destructive guard exits 1 with empty stderr after 17s — a live fail-open distinct from #1242 #1423 record:exitCode: 1,durationMs: 17054) and by a manual smoke test against a genuine local transcript record (see Testplan) that reproduces
exitCode: 1,durationMs: 11, and the real config-refusal stderr text.permissionDecisionordecision: block; on any transcript read/parsefailure it exits 0 with no output. The once-per-session marker degrades toward re-warning, never
toward silence, if its own bookkeeping write fails — over-warning is the safe direction for a module
whose entire purpose is killing a silent-suppression defect class.
plugin.json0.9.4 → 0.9.5,CHANGELOG.mdentry,README.mdandskills/clean/reference/safety-model.mdboth state what's covered (onlydestructive_guard.py'sown command string, current-session only) and what isn't (repo-hygiene's own guard — verified
working separately; no retroactive scan of past sessions).
Test plan
plugins/disk-hygiene/skills/clean/scripts/guard_launch_monitor.test.sh— 17/17 pass:the disk-hygiene: destructive guard exits 1 with empty stderr after 17s — a live fail-open distinct from #1242 #1423 shape (states
exitCode: 1/durationMs: 17054in the rendered string), thelaunch-refusal shape, empty-stderr placeholder rendering, a clean session with a different
hook's failure present (proves the command-substring filter discriminates), a fully clean session,
malformed/unreadable transcript, missing
transcript_path, malformed stdin, once-per-sessionsuppression (same session id) vs independent warnings (different session ids), tail-bounded read
still finds a failure near the end of an oversized transcript, marker-write failure still emits the
warning this run, and a direct assertion that no
permissionDecision/decision: blockis everemitted.
piped a real
hook_non_blocking_errorrecord fordestructive_guard.py(
exitCode: 1,durationMs: 11, the real "Plugin option "disk_hygiene_enabled" isn't set"stderr) through the finished detector — emitted
systemMessagenames all three correctly.bash scripts/check-hook-userconfig-argv.sh— pass (new hook's args carry no${user_config.*}token).
bash scripts/check-changelog-parity.sh --check-bump origin/main— pass.node scripts/validate-plugin-contracts.mjs— pass (43 setup skills, 2101 plugin files).claude plugin validate plugins/disk-hygiene/— pass.bash scripts/run-plugin-tests.sh(full repo, 149*.test.shfiles) — run locally; time-boxedpartway through (29/149 files, 0 failures) given this change's isolation to new disk-hygiene-only
files plus the repo-wide structural gates above already passing across all 2101 plugin files. CI
runs the same script to completion as the authoritative full-repo gate.
Related
Refs #1423 — the live launch/runtime-death fail-open this issue was found alongside, fixed separately
by #1449 (open, unmerged as of this PR).
Refs #1449 — open PR, unmerged, also touches
plugins/disk-hygiene/.claude-plugin/plugin.json,CHANGELOG.md, andhooks/hooks.jsonfor the #1423 fix. Both PRs edit the same three files; whoevermerges second should expect a straightforward rebase (this PR adds a new
Stophooks.json key and anew CHANGELOG/version entry — no overlapping lines with #1449's
PreToolUse-side edit, but git maystill want a manual pass).
🤖 Generated with Claude Code
This was generated by AI during work-loop execution.