From 06aacf3141b38c74ac652bcb0ad0da0cc5cadfd1 Mon Sep 17 00:00:00 2001
From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com>
Date: Tue, 15 Sep 2026 19:32:50 -0400
Subject: [PATCH 1/3] perf(disk-hygiene): skip the Stop monitor when no guard
launched
The engine-gate rows are `if`-gated on the engine's file name, so most
sessions never launch destructive_guard.py at all, yet the Stop monitor
started a whole Python every turn to discover that from the transcript.
Stop rows accept neither `matcher` nor `if`, so the gate has to live in
the bash launcher, before the interpreter is resolved.
run-python-hook.sh takes three optional leading flags, consumed there and
never forwarded to Python:
--marker-root
root the marker tree lives under
--launch-marker write //.launched
before exec'ing the target
--skip-unless-marker exit 0 without exec'ing when it is absent
The root is passed explicitly rather than read from CLAUDE_PLUGIN_DATA in
the environment, which a hook subprocess can inherit naming a different
plugin's data directory; a writer and a reader that disagree about the
root skip silently, which is the missed detection this monitor exists to
prevent. The session id is recovered by a regex anchored to the payload's
opening key, so nothing nested inside the payload can reach it, with an
unanchored fallback so a reordered payload degrades to running Python
rather than to keying on nothing; a payload it cannot key on records
nothing and skips nothing. The payload is buffered with `read` and
replayed on a here-string, so Python reads the same bytes plus the newline
`<<<` appends; both consumers ignore trailing whitespace.
Process creations per Stop on Windows, job-object census at n=5 against a
1-creation harness floor: 5 before, 3 with no marker, 5 with one present.
Wall clock 271 ms to 120 ms on the skipped path. The engine-gate row pays
nothing for the marker after the first launch in a data root, which spends
one mkdir (7 creations once, then 5). With a marker present the monitor's
systemMessage and its guard-decisions record are byte-identical.
The marker is written before the interpreter resolves, so a guard that
launches and dies still leaves it. Markers are never removed and nothing
sweeps the data root; a session whose guard rows never fired is skipped by
design, and a skipped turn emits no telemetry envelope where it used to
emit an `ok` one.
run-python-hook.test.sh gains eighteen assertions over the four halves of
that contract, and stubs `py` alongside python3/python in the no-python
case, which a host carrying the Windows py launcher resolved through.
Co-Authored-By: Claude Fable 5.1
---
.../disk-hygiene/.claude-plugin/plugin.json | 2 +-
plugins/disk-hygiene/CHANGELOG.md | 7 +
plugins/disk-hygiene/hooks/hooks.json | 10 +-
plugins/disk-hygiene/hooks/run-python-hook.sh | 151 ++++++++++++++++++
.../hooks/run-python-hook.test.sh | 125 ++++++++++++++-
5 files changed, 288 insertions(+), 7 deletions(-)
diff --git a/plugins/disk-hygiene/.claude-plugin/plugin.json b/plugins/disk-hygiene/.claude-plugin/plugin.json
index 79241c891f..c9a54dcdf8 100644
--- a/plugins/disk-hygiene/.claude-plugin/plugin.json
+++ b/plugins/disk-hygiene/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "disk-hygiene",
- "version": "0.23.11",
+ "version": "0.23.12",
"description": "Context-aware disk hygiene for arbitrary directory trees: inventories orphaned and temporary artifacts, classifies evidence into review tiers, and offers exact-path cleanup only after a fresh safety preview and explicit per-tier approval. The target is read-only by default; OS-managed paths, links and mount points, VCS-tracked content without the complete checkout evidence bundle, changed entries, and live-handle uncertainty fail closed.",
"author": {
"name": "Melodic Software",
diff --git a/plugins/disk-hygiene/CHANGELOG.md b/plugins/disk-hygiene/CHANGELOG.md
index f3f1f60470..e594510e49 100644
--- a/plugins/disk-hygiene/CHANGELOG.md
+++ b/plugins/disk-hygiene/CHANGELOG.md
@@ -3,6 +3,13 @@
All notable changes to the `disk-hygiene` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.
+## [0.23.12]
+
+### Changed
+
+- **The `Stop` guard-launch monitor no longer starts Python in a session that launched no guard.** The engine-gate rows are `if`-gated on the engine's file name, so most sessions never run `destructive_guard.py` at all, yet the monitor started a whole interpreter every turn to discover that from the transcript. `Stop` rows accept neither `matcher` nor `if`, so the gate lives in `hooks/run-python-hook.sh`, which grows three optional leading flags: `--marker-root `, `--launch-marker ` (write `//.launched` before exec'ing Python) and `--skip-unless-marker ` (exit 0 without exec'ing when that file is absent). Process creations per `Stop` on Windows, measured with a job-object census at n=5: 5 before, 3 with no marker, and still 5 with one present, against a 1-creation harness floor; wall clock 271 ms to 120 ms on the skipped path. The engine-gate row pays nothing for the marker after the first launch in a plugin data root, which spends one `mkdir`.
+- Marker semantics: the marker is per session and is never removed, so a data root accumulates one empty file per session that launched a guard, and no retention sweep collects them; a session whose guard rows never fired is skipped by design; the marker is written before the interpreter is resolved, so a guard that launches and dies, the failure the monitor exists to report, still leaves it. With a marker present the monitor's `systemMessage` and its `guard-decisions` record are byte-identical to before. A skipped turn emits no telemetry envelope at all, where it previously emitted an `ok` one.
+
## [0.23.11]
### Changed
diff --git a/plugins/disk-hygiene/hooks/hooks.json b/plugins/disk-hygiene/hooks/hooks.json
index 2c606707fb..a51996d632 100644
--- a/plugins/disk-hygiene/hooks/hooks.json
+++ b/plugins/disk-hygiene/hooks/hooks.json
@@ -7,7 +7,7 @@
"hooks": [
{
"type": "command",
- "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh --marker-root \"${CLAUDE_PLUGIN_DATA}\" --launch-marker guard-launch-monitor \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
"if": "Bash(*hygiene.py*)",
"shell": "bash",
"timeout": 60,
@@ -20,7 +20,7 @@
"hooks": [
{
"type": "command",
- "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh --marker-root \"${CLAUDE_PLUGIN_DATA}\" --launch-marker guard-launch-monitor \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
"if": "PowerShell(*hygiene.py*)",
"shell": "bash",
"timeout": 60,
@@ -28,7 +28,7 @@
},
{
"type": "command",
- "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh --marker-root \"${CLAUDE_PLUGIN_DATA}\" --launch-marker guard-launch-monitor \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
"if": "PowerShell(*python*$*)",
"shell": "bash",
"timeout": 60,
@@ -36,7 +36,7 @@
},
{
"type": "command",
- "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh --marker-root \"${CLAUDE_PLUGIN_DATA}\" --launch-marker guard-launch-monitor \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/destructive_guard.py --mode engine-gate --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --authorized-data-root \"${CLAUDE_PLUGIN_DATA}\"",
"if": "PowerShell(*& $*)",
"shell": "bash",
"timeout": 60,
@@ -50,7 +50,7 @@
"hooks": [
{
"type": "command",
- "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/guard_launch_monitor.py --data-root \"${CLAUDE_PLUGIN_DATA}\"",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/run-python-hook.sh --marker-root \"${CLAUDE_PLUGIN_DATA}\" --skip-unless-marker guard-launch-monitor \"${CLAUDE_PLUGIN_ROOT}\"/skills/clean/scripts/guard_launch_monitor.py --data-root \"${CLAUDE_PLUGIN_DATA}\"",
"shell": "bash",
"timeout": 20,
"statusMessage": "Checking for silent destructive-guard failures..."
diff --git a/plugins/disk-hygiene/hooks/run-python-hook.sh b/plugins/disk-hygiene/hooks/run-python-hook.sh
index ceea8e7184..5080ff9619 100755
--- a/plugins/disk-hygiene/hooks/run-python-hook.sh
+++ b/plugins/disk-hygiene/hooks/run-python-hook.sh
@@ -81,6 +81,77 @@ except OSError:
raise SystemExit(0 if sys.version_info >= floor else 1)
'
+# --- optional per-session launch marker ------------------------------------
+#
+# Three OPTIONAL leading flags, consumed here and never forwarded to Python:
+#
+# --marker-root root the marker tree lives under, spelled
+# `"${CLAUDE_PLUGIN_DATA}"` by every caller;
+# --launch-marker write `//.launched`
+# BEFORE exec'ing the target;
+# --skip-unless-marker exit 0 without exec'ing anything when that
+# file is absent.
+#
+# Together they let a `Stop` hook cost nothing in a session where the hook it
+# watches never ran. `guard_launch_monitor.py` reports failures of the
+# `PreToolUse` guard, and the guard rows are `if`-gated on the engine's file
+# name, so most sessions never launch the guard at all — yet the monitor still
+# started a whole Python on every turn to discover that. `Stop` rows accept
+# neither `matcher` nor `if`, so the only place that gate can live is here.
+#
+# The root is passed EXPLICITLY rather than read from `CLAUDE_PLUGIN_DATA` in
+# the environment: a hook subprocess can inherit that variable naming a
+# DIFFERENT plugin's data directory, and a writer and a reader that disagree
+# about the root skip silently, which is the missed-detection failure this
+# monitor exists to prevent. An empty value, or an unsubstituted literal
+# `${CLAUDE_PLUGIN_DATA}`, is treated as absent (the placeholder idiom
+# `guard_launch_monitor.py` already uses) and the tmp fallback carries the
+# marker alone.
+#
+# Failure modes, stated rather than implied:
+# * a marker is per session and is never removed. Under the tmp fallback the
+# OS clears it; under `--marker-root` it is one empty file per session that
+# launched a guard, and neither the clean engine nor `lib/guard_decision_log.py`
+# runs a retention sweep over the plugin data root that could collect them.
+# * a session whose guard rows never fired is skipped BY DESIGN: there is no
+# guard invocation for the monitor to have found a failure of.
+# * a hook that failed before this script ran leaves no marker and is not this
+# monitor's to detect — the detector reads the transcript for the guard's
+# own command string, which such a failure still records.
+# * the marker is written before the interpreter is even resolved, so the
+# failure the monitor DOES exist to catch — the guard launching and dying —
+# still leaves the marker that keeps the monitor running.
+# * the session id is recovered with a regex over the raw payload rather than
+# a JSON parse, because a parse is the Python this gate exists to avoid.
+# The match is anchored to the payload's opening key, which nothing further
+# in the payload can reach; only a payload that does not open with
+# `session_id` falls back to an unanchored match, where a NESTED
+# `session_id` key could win instead and key the marker wrongly. Both rows
+# parse identically, so that corner is a missed detection, never a false
+# alarm.
+# * a mis-parse fails SAFE: no session id means write nothing and skip
+# nothing, which is this launcher's behavior before these flags existed.
+MARKER_ROOT=""
+LAUNCH_MARKER_SUBDIR=""
+SKIP_MARKER_SUBDIR=""
+while (($#)); do
+ case "$1" in
+ --marker-root)
+ MARKER_ROOT="${2:-}"
+ shift 2 || break
+ ;;
+ --launch-marker)
+ LAUNCH_MARKER_SUBDIR="${2:-}"
+ shift 2 || break
+ ;;
+ --skip-unless-marker)
+ SKIP_MARKER_SUBDIR="${2:-}"
+ shift 2 || break
+ ;;
+ *) break ;;
+ esac
+done
+
SCRIPT="${1:-}"
shift || true
@@ -90,6 +161,77 @@ case "$SCRIPT" in
*) ;;
esac
+# shellcheck disable=SC2016 # the literal placeholder, deliberately unexpanded
+_DATA_ROOT_PLACEHOLDER='${CLAUDE_PLUGIN_DATA}'
+_MARKER_PATHS=()
+_PAYLOAD=""
+_SESSION_ID=""
+_BUFFERED_STDIN=0
+
+# Buffer the whole hook payload into a variable and recover its session id.
+# `read` is a builtin, so buffering costs no process; the payload is handed to
+# Python on a here-string at `exec` time, leaving its stdin unchanged.
+_read_payload() {
+ _BUFFERED_STDIN=1
+ IFS= read -r -d '' _PAYLOAD || true
+ local field='"session_id"[[:space:]]*:[[:space:]]*"([^"\\]*)"'
+ # Anchored first. The hooks reference shows `session_id` as the payload's
+ # opening key for every event but documents no ordering guarantee, and the
+ # payload nests objects of its own (`tool_input`) that could carry the same
+ # key name. An anchored match cannot be reached by either; the unanchored
+ # fallback runs only for a payload that did NOT open with the field, and keeps
+ # a reordered payload from turning this gate into a silent no-op.
+ local anchored="^[[:space:]]*[{][[:space:]]*$field"
+ if [[ "$_PAYLOAD" =~ $anchored ]] || [[ "$_PAYLOAD" =~ $field ]]; then
+ _SESSION_ID="${BASH_REMATCH[1]}"
+ fi
+}
+
+# Fill `_MARKER_PATHS` with the candidates for one subdir, most-preferred
+# first, mirroring `guard_launch_monitor.py`'s own `_marker_path_candidates`:
+# the data root when one resolved, then a tmp fallback whose directory name is
+# built from the subdir, so a `.launched` marker sits beside the `.warned` one
+# the monitor writes. Only bash reads and writes `.launched`, so the tmp
+# directory need not be the one Python's `tempfile.gettempdir()` picks; what
+# matters is that both flags resolve it identically, which they do.
+_marker_candidates() {
+ local subdir="$1" session="$2"
+ local safe="${session//[^a-zA-Z0-9_-]/_}"
+ _MARKER_PATHS=()
+ [[ -n "$safe" ]] || return 1
+ if [[ -n "$MARKER_ROOT" && "$MARKER_ROOT" != "$_DATA_ROOT_PLACEHOLDER" ]]; then
+ _MARKER_PATHS+=("$MARKER_ROOT/$subdir/$safe.launched")
+ fi
+ _MARKER_PATHS+=("${TMPDIR:-/tmp}/disk-hygiene-$subdir/$safe.launched")
+}
+
+if [[ -n "$SKIP_MARKER_SUBDIR" ]]; then
+ _read_payload
+ if [[ -n "$_SESSION_ID" ]] && _marker_candidates "$SKIP_MARKER_SUBDIR" "$_SESSION_ID"; then
+ _marker_found=0
+ for _marker_path in "${_MARKER_PATHS[@]}"; do
+ if [[ -f "$_marker_path" ]]; then
+ _marker_found=1
+ break
+ fi
+ done
+ ((_marker_found)) || exit 0
+ fi
+fi
+
+if [[ -n "$LAUNCH_MARKER_SUBDIR" ]]; then
+ ((_BUFFERED_STDIN)) || _read_payload
+ if [[ -n "$_SESSION_ID" ]] && _marker_candidates "$LAUNCH_MARKER_SUBDIR" "$_SESSION_ID"; then
+ for _marker_path in "${_MARKER_PATHS[@]}"; do
+ _marker_dir="${_marker_path%/*}"
+ # `mkdir` is the one spawn on this path and `[[ -d ]]` is a builtin, so it
+ # is paid once per root rather than once per launch.
+ [[ -d "$_marker_dir" ]] || mkdir -p "$_marker_dir" 2>/dev/null || continue
+ : >"$_marker_path" 2>/dev/null && break
+ done
+ fi
+fi
+
# Portable WindowsApps path-component check (case-insensitive).
_under_windowsapps() {
[[ "${1,,}" == *windowsapps* ]]
@@ -315,4 +457,13 @@ if [[ -z "$PYTHON" ]]; then
exit 0
fi
+# A buffered payload is replayed on a here-string, which bash serves from a pipe
+# or a temp file without creating a process. Python reads the same bytes it
+# would have read from the inherited stdin, plus the newline `<<<` appends —
+# both consumers here (`json.load(sys.stdin)` in the guard, `sys.stdin.read()`
+# then `json.loads` in the monitor) ignore trailing whitespace.
+if ((_BUFFERED_STDIN)); then
+ exec "$PYTHON" "$SCRIPT" "$@" <<<"$_PAYLOAD"
+fi
+
exec "$PYTHON" "$SCRIPT" "$@"
diff --git a/plugins/disk-hygiene/hooks/run-python-hook.test.sh b/plugins/disk-hygiene/hooks/run-python-hook.test.sh
index b9d0c77158..582c89fc84 100755
--- a/plugins/disk-hygiene/hooks/run-python-hook.test.sh
+++ b/plugins/disk-hygiene/hooks/run-python-hook.test.sh
@@ -381,7 +381,10 @@ done
FAKE_BIN="$(mktemp -d)"
# Replaces (does not chain onto) the earlier EXIT trap, so it cleans up both.
trap 'rm -rf "$FAKE_BIN" "$PROBE_DIR"' EXIT
-for stub in python3 python; do
+# `py` belongs in the stub set with the other two: the launcher's third branch
+# resolves through the Windows `py` launcher, and a host that has one resolves a
+# real interpreter here, runs the monitor, and sees no systemMessage at all.
+for stub in python3 python py; do
printf '#!/usr/bin/env bash\nexit 127\n' >"$FAKE_BIN/$stub"
chmod +x "$FAKE_BIN/$stub"
done
@@ -458,4 +461,124 @@ else
"2" "$census_execs"
fi
+# --- the per-session launch marker gates the Stop monitor before python ---
+#
+# `Stop` rows accept neither `matcher` nor `if`, so the only place a "did the
+# guard run at all this session" gate can live is the launcher. The contract has
+# four halves: a launch records the session (even when the launched python then
+# dies, which is the very failure the monitor exists to report), an unrecorded
+# session reaches no python at all, a recorded one runs exactly as before, and a
+# payload the launcher cannot key on falls back to running python.
+MARKER_CASE="$PROBE_DIR/marker-case"
+MARKER_ROOT_DIR="$MARKER_CASE/data"
+MARKER_HOME="$MARKER_CASE/home"
+MARKER_SEEN="$MARKER_CASE/target-stdin"
+MARKER_DIR="$MARKER_ROOT_DIR/guard-launch-monitor"
+mkdir -p "$MARKER_ROOT_DIR" "$MARKER_HOME" "$MARKER_CASE/tmp"
+MARKER_TARGET="$FIXTURE_ROOT/skills/clean/scripts/marker_target.py"
+{
+ printf 'import pathlib, sys\n'
+ # Byte-for-byte, through the binary buffer: the payload reaches python over a
+ # here-string now, and a decoded round trip would hide a newline difference.
+ printf 'pathlib.Path(sys.argv[1]).write_bytes(sys.stdin.buffer.read())\n'
+ printf 'pathlib.Path(sys.argv[1] + ".argv").write_text("\\n".join(sys.argv[3:]))\n'
+ printf 'raise SystemExit(int(sys.argv[2]))\n'
+} >"$MARKER_TARGET"
+
+MARKER_PAYLOAD=""
+# Echo the launcher's exit code; `$MARKER_SEEN` exists afterwards only if the
+# target actually started, and holds the bytes python read.
+marker_launch() {
+ local flag="$1" subdir="$2" target_rc="$3" rc=0
+ rm -f "$MARKER_SEEN" "$MARKER_SEEN.argv"
+ printf '%s' "$MARKER_PAYLOAD" |
+ HOME="$MARKER_HOME" TMPDIR="$MARKER_CASE/tmp" \
+ bash "$FIXTURE_ROOT/hooks/run-python-hook.sh" \
+ --marker-root "$MARKER_ROOT_DIR" "$flag" "$subdir" \
+ "$MARKER_TARGET" "$MARKER_SEEN" "$target_rc" >/dev/null 2>&1 || rc=$?
+ printf '%s' "$rc"
+}
+
+marker_state() {
+ [[ -f "$MARKER_DIR/$1.launched" ]] && printf 'present' || printf 'absent'
+}
+
+target_state() {
+ [[ -e "$MARKER_SEEN" ]] && printf 'ran' || printf 'skipped'
+}
+
+MARKER_PAYLOAD='{"session_id":"sess-1","hook_event_name":"PreToolUse"}'
+rm -rf "$MARKER_DIR"
+marker_rc="$(marker_launch --launch-marker guard-launch-monitor 0)"
+assert_eq "a guarded launch records the session" "present" "$(marker_state sess-1)"
+assert_eq "a guarded launch still runs its target" "ran" "$(target_state)"
+assert_eq "a guarded launch still reports its target's exit code" "0" "$marker_rc"
+assert_eq "the marker flags never reach python's argv" "" "$(cat "$MARKER_SEEN.argv")"
+
+# The monitor exists to catch a guard that launched and died, so a non-zero exit
+# must leave the marker that keeps the monitor running for the rest of the turn.
+rm -rf "$MARKER_DIR"
+marker_rc="$(marker_launch --launch-marker guard-launch-monitor 3)"
+assert_eq "a launch whose python exits non-zero still records the session" \
+ "present" "$(marker_state sess-1)"
+assert_eq "a launch whose python exits non-zero propagates that code" "3" "$marker_rc"
+
+MARKER_PAYLOAD='{"session_id":"sess-1","hook_event_name":"Stop"}'
+rm -rf "$MARKER_DIR"
+marker_rc="$(marker_launch --skip-unless-marker guard-launch-monitor 0)"
+assert_eq "an unrecorded session exits 0" "0" "$marker_rc"
+assert_eq "an unrecorded session starts no python" "skipped" "$(target_state)"
+
+mkdir -p "$MARKER_DIR"
+: >"$MARKER_DIR/sess-1.launched"
+marker_rc="$(marker_launch --skip-unless-marker guard-launch-monitor 0)"
+assert_eq "a recorded session runs python" "ran" "$(target_state)"
+assert_eq "a recorded session exits 0" "0" "$marker_rc"
+
+# Byte identity across the buffer-and-replay: python receives what it would have
+# read from the inherited stdin, plus the newline `<<<` appends. Both consumers
+# (`json.load(sys.stdin)`, `sys.stdin.read()` then `json.loads`) ignore it.
+MARKER_EXPECTED="$MARKER_CASE/expected"
+printf '%s\n' "$MARKER_PAYLOAD" >"$MARKER_EXPECTED"
+assert_eq "the buffered payload reaches python unchanged" "same" \
+ "$(cmp -s "$MARKER_EXPECTED" "$MARKER_SEEN" && printf 'same' || printf 'differs')"
+
+# A payload the launcher cannot key on must behave exactly as it did before
+# these flags existed: record nothing, skip nothing, run python.
+MARKER_PAYLOAD='{"hook_event_name":"Stop","cwd":"/tmp"}'
+rm -rf "$MARKER_DIR"
+marker_rc="$(marker_launch --skip-unless-marker guard-launch-monitor 0)"
+assert_eq "a payload with no session id still runs python" "ran" "$(target_state)"
+assert_eq "a payload with no session id exits 0" "0" "$marker_rc"
+marker_launch --launch-marker guard-launch-monitor 0 >/dev/null
+assert_eq "a payload with no session id records nothing" "absent" \
+ "$([[ -d "$MARKER_DIR" ]] && printf 'present' || printf 'absent')"
+
+# A payload whose opening key is something else still keys correctly, through
+# the unanchored fallback: the docs show `session_id` first for every event but
+# guarantee no ordering, and a reordered payload must degrade to running python,
+# not to keying on nothing.
+MARKER_PAYLOAD='{"hook_event_name":"Stop","session_id":"sess-2"}'
+rm -rf "$MARKER_DIR"
+marker_launch --skip-unless-marker guard-launch-monitor 0 >/dev/null
+assert_eq "a reordered payload still keys on its session id" "skipped" "$(target_state)"
+mkdir -p "$MARKER_DIR"
+: >"$MARKER_DIR/sess-2.launched"
+marker_launch --skip-unless-marker guard-launch-monitor 0 >/dev/null
+assert_eq "a reordered payload finds its own marker" "ran" "$(target_state)"
+
+# --- both wirings carry the flag the other one depends on ---
+guard_rows="$(jq '[.hooks.PreToolUse[].hooks[] |
+ select(.command | contains("destructive_guard.py"))] | length' "$HOOKS_JSON")"
+guard_marked="$(jq '[.hooks.PreToolUse[].hooks[] |
+ select(.command | contains("destructive_guard.py")) |
+ select(.command | contains("--launch-marker guard-launch-monitor"))] | length' \
+ "$HOOKS_JSON")"
+assert_eq "every engine-gate row records the session it launched in" \
+ "$guard_rows" "$guard_marked"
+assert_eq "the Stop row skips a session that launched no guard" "1" \
+ "$(jq '[.hooks.Stop[].hooks[] |
+ select(.command | contains("--skip-unless-marker guard-launch-monitor"))] | length' \
+ "$HOOKS_JSON")"
+
pass "all run-python-hook contract checks"
From f6028aa7c440afdc6b9c8b9110d3d30e83b5ff92 Mon Sep 17 00:00:00 2001
From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com>
Date: Tue, 15 Sep 2026 20:16:42 -0400
Subject: [PATCH 2/3] docs(disk-hygiene): spell misparse the way the typo check
expects
CI's typos lane read the hyphenated form as a misspelling of miss.
Co-Authored-By: Claude Fable 5.1
---
plugins/disk-hygiene/hooks/run-python-hook.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/disk-hygiene/hooks/run-python-hook.sh b/plugins/disk-hygiene/hooks/run-python-hook.sh
index 5080ff9619..5cb7d6a51a 100755
--- a/plugins/disk-hygiene/hooks/run-python-hook.sh
+++ b/plugins/disk-hygiene/hooks/run-python-hook.sh
@@ -129,7 +129,7 @@ raise SystemExit(0 if sys.version_info >= floor else 1)
# `session_id` key could win instead and key the marker wrongly. Both rows
# parse identically, so that corner is a missed detection, never a false
# alarm.
-# * a mis-parse fails SAFE: no session id means write nothing and skip
+# * a misparse fails SAFE: no session id means write nothing and skip
# nothing, which is this launcher's behavior before these flags existed.
MARKER_ROOT=""
LAUNCH_MARKER_SUBDIR=""
From 05d2656e1ccbc23ac61afd4877174925ad859b7a Mon Sep 17 00:00:00 2001
From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com>
Date: Tue, 15 Sep 2026 22:29:52 -0400
Subject: [PATCH 3/3] fix(disk-hygiene): run the Stop monitor when no marker
directory exists
The Stop gate skipped the guard-launch monitor whenever no marker file was
found, so a launch whose mkdir failed for both candidate roots silenced the
monitor for the rest of the session: a silent failure in the one detector
that exists to report silent failures. The gate now skips only when at least
one candidate marker directory exists and none of them holds a marker for
the session, and otherwise runs the monitor exactly as the launcher did
before these flags. Both tests are bash builtins, so the skip path still
reaches its exit without spawning anything.
The residual is stated rather than implied, in the launcher header and in
the changelog: a candidate directory that exists while the marker file
itself could not be written still degrades to silence for that session,
because that file is the only channel between the launch row and the Stop
row. The fail-open also makes the skip inert in a plugin data root where no
guard has ever launched, until the first launch spends its mkdir.
The suite stages an unwritable marker root structurally rather than through
permission bits MSYS cannot set against Windows ACLs: mkdir -p refuses a
parent that is a regular file, and refuses a target that already exists as
one. New cases cover a launch that can write no marker at all, the Stop that
follows it, and a candidate directory holding no marker for the session.
Process creations per Stop on Windows, job-object census at n=5: 5 with no
candidate directory, 3 with an empty one, 5 with a marker present, against a
1-creation harness floor. The engine-gate launch path is unchanged at 5
after the first launch in a data root spends its mkdir.
Co-Authored-By: Claude Fable 5.1
---
plugins/disk-hygiene/CHANGELOG.md | 3 +-
plugins/disk-hygiene/hooks/run-python-hook.sh | 31 ++++++++-
.../hooks/run-python-hook.test.sh | 65 +++++++++++++++++--
3 files changed, 91 insertions(+), 8 deletions(-)
diff --git a/plugins/disk-hygiene/CHANGELOG.md b/plugins/disk-hygiene/CHANGELOG.md
index e594510e49..b96edd1ab4 100644
--- a/plugins/disk-hygiene/CHANGELOG.md
+++ b/plugins/disk-hygiene/CHANGELOG.md
@@ -7,8 +7,9 @@ All notable changes to the `disk-hygiene` plugin are documented here. Format fol
### Changed
-- **The `Stop` guard-launch monitor no longer starts Python in a session that launched no guard.** The engine-gate rows are `if`-gated on the engine's file name, so most sessions never run `destructive_guard.py` at all, yet the monitor started a whole interpreter every turn to discover that from the transcript. `Stop` rows accept neither `matcher` nor `if`, so the gate lives in `hooks/run-python-hook.sh`, which grows three optional leading flags: `--marker-root `, `--launch-marker ` (write `//.launched` before exec'ing Python) and `--skip-unless-marker ` (exit 0 without exec'ing when that file is absent). Process creations per `Stop` on Windows, measured with a job-object census at n=5: 5 before, 3 with no marker, and still 5 with one present, against a 1-creation harness floor; wall clock 271 ms to 120 ms on the skipped path. The engine-gate row pays nothing for the marker after the first launch in a plugin data root, which spends one `mkdir`.
+- **The `Stop` guard-launch monitor no longer starts Python in a session that launched no guard.** The engine-gate rows are `if`-gated on the engine's file name, so most sessions never run `destructive_guard.py` at all, yet the monitor started a whole interpreter every turn to discover that from the transcript. `Stop` rows accept neither `matcher` nor `if`, so the gate lives in `hooks/run-python-hook.sh`, which grows three optional leading flags: `--marker-root `, `--launch-marker ` (write `//.launched` before exec'ing Python) and `--skip-unless-marker ` (exit 0 without exec'ing when a candidate marker directory exists and none of them holds that file). Process creations per `Stop` on Windows, measured with a job-object census at n=5: 5 before, 3 with no marker, and still 5 with one present, against a 1-creation harness floor; wall clock 271 ms to 120 ms on the skipped path. The engine-gate row pays nothing for the marker after the first launch in a plugin data root, which spends one `mkdir`.
- Marker semantics: the marker is per session and is never removed, so a data root accumulates one empty file per session that launched a guard, and no retention sweep collects them; a session whose guard rows never fired is skipped by design; the marker is written before the interpreter is resolved, so a guard that launches and dies, the failure the monitor exists to report, still leaves it. With a marker present the monitor's `systemMessage` and its `guard-decisions` record are byte-identical to before. A skipped turn emits no telemetry envelope at all, where it previously emitted an `ok` one.
+- The `Stop` gate fails open on the half it can detect: it skips only when at least one candidate marker directory exists and holds no marker for the session, so a launch whose `mkdir` failed for both roots leaves nothing and every later `Stop` runs the monitor as it did before the flags. The cost is that the skip is inert in a plugin data root where no guard has ever launched, until the first launch spends its `mkdir`: 5 creations with no candidate directory, 3 with an empty one, 5 with a marker present. The residual it cannot detect: a candidate directory that exists while the marker file itself could not be written (a full disk, a permission denial on the file alone) still silences the monitor for that session, because that file is the only channel between the launch row and the `Stop` row.
## [0.23.11]
diff --git a/plugins/disk-hygiene/hooks/run-python-hook.sh b/plugins/disk-hygiene/hooks/run-python-hook.sh
index 5cb7d6a51a..6b6ae67438 100755
--- a/plugins/disk-hygiene/hooks/run-python-hook.sh
+++ b/plugins/disk-hygiene/hooks/run-python-hook.sh
@@ -89,8 +89,9 @@ raise SystemExit(0 if sys.version_info >= floor else 1)
# `"${CLAUDE_PLUGIN_DATA}"` by every caller;
# --launch-marker write `//.launched`
# BEFORE exec'ing the target;
-# --skip-unless-marker exit 0 without exec'ing anything when that
-# file is absent.
+# --skip-unless-marker exit 0 without exec'ing anything when at
+# least one candidate directory exists and
+# none of them holds that file.
#
# Together they let a `Stop` hook cost nothing in a session where the hook it
# watches never ran. `guard_launch_monitor.py` reports failures of the
@@ -131,6 +132,21 @@ raise SystemExit(0 if sys.version_info >= floor else 1)
# alarm.
# * a misparse fails SAFE: no session id means write nothing and skip
# nothing, which is this launcher's behavior before these flags existed.
+# * a launch that could create NEITHER candidate directory leaves no marker
+# at all, and a Stop that read only the marker file would then silence the
+# monitor for the whole session: a silent failure in the one detector that
+# exists to catch silent failures. So the skip is gated on a candidate
+# DIRECTORY existing. With none, the Stop row falls through and runs the
+# monitor, which is the behavior before these flags existed. The cost of
+# that fail-open is that the skip is inert in a plugin data root where no
+# guard has ever launched: every Stop pays the old price until the first
+# guard launch spends its one `mkdir`.
+# * RESIDUAL, and undetectable across processes: a candidate directory that
+# exists while the marker FILE could not be written (a full disk, a
+# permission denial on the file alone) still degrades to silence for that
+# session. The file that failed is the only channel between the launch row
+# and the Stop row, so the Stop row cannot tell that case apart from a
+# session whose guard rows never fired.
MARKER_ROOT=""
LAUNCH_MARKER_SUBDIR=""
SKIP_MARKER_SUBDIR=""
@@ -209,13 +225,22 @@ if [[ -n "$SKIP_MARKER_SUBDIR" ]]; then
_read_payload
if [[ -n "$_SESSION_ID" ]] && _marker_candidates "$SKIP_MARKER_SUBDIR" "$_SESSION_ID"; then
_marker_found=0
+ # Skipping requires POSITIVE evidence that the launch side got as far as a
+ # marker tree. No candidate directory at all is the shape a launch whose
+ # `mkdir` failed for every root leaves behind, and silencing the monitor on
+ # it would be a silent failure in the detector that exists to catch silent
+ # failures. Both tests are builtins, so the skip path still spawns nothing.
+ _marker_dir_seen=0
for _marker_path in "${_MARKER_PATHS[@]}"; do
+ [[ -d "${_marker_path%/*}" ]] && _marker_dir_seen=1
if [[ -f "$_marker_path" ]]; then
_marker_found=1
break
fi
done
- ((_marker_found)) || exit 0
+ if ((_marker_dir_seen && !_marker_found)); then
+ exit 0
+ fi
fi
fi
diff --git a/plugins/disk-hygiene/hooks/run-python-hook.test.sh b/plugins/disk-hygiene/hooks/run-python-hook.test.sh
index 582c89fc84..3add1588d9 100755
--- a/plugins/disk-hygiene/hooks/run-python-hook.test.sh
+++ b/plugins/disk-hygiene/hooks/run-python-hook.test.sh
@@ -465,10 +465,12 @@ fi
#
# `Stop` rows accept neither `matcher` nor `if`, so the only place a "did the
# guard run at all this session" gate can live is the launcher. The contract has
-# four halves: a launch records the session (even when the launched python then
-# dies, which is the very failure the monitor exists to report), an unrecorded
-# session reaches no python at all, a recorded one runs exactly as before, and a
-# payload the launcher cannot key on falls back to running python.
+# five halves: a launch records the session (even when the launched python then
+# dies, which is the very failure the monitor exists to report), a session
+# unrecorded in a marker tree that EXISTS reaches no python at all, a recorded
+# one runs exactly as before, a payload the launcher cannot key on falls back to
+# running python, and a marker tree that does not exist at all falls back the
+# same way rather than silencing the monitor.
MARKER_CASE="$PROBE_DIR/marker-case"
MARKER_ROOT_DIR="$MARKER_CASE/data"
MARKER_HOME="$MARKER_CASE/home"
@@ -523,8 +525,11 @@ assert_eq "a launch whose python exits non-zero still records the session" \
"present" "$(marker_state sess-1)"
assert_eq "a launch whose python exits non-zero propagates that code" "3" "$marker_rc"
+# An EXISTING marker tree that holds no marker for this session is the shape a
+# launch-free session leaves, and is the only shape that may skip.
MARKER_PAYLOAD='{"session_id":"sess-1","hook_event_name":"Stop"}'
rm -rf "$MARKER_DIR"
+mkdir -p "$MARKER_DIR"
marker_rc="$(marker_launch --skip-unless-marker guard-launch-monitor 0)"
assert_eq "an unrecorded session exits 0" "0" "$marker_rc"
assert_eq "an unrecorded session starts no python" "skipped" "$(target_state)"
@@ -560,6 +565,7 @@ assert_eq "a payload with no session id records nothing" "absent" \
# not to keying on nothing.
MARKER_PAYLOAD='{"hook_event_name":"Stop","session_id":"sess-2"}'
rm -rf "$MARKER_DIR"
+mkdir -p "$MARKER_DIR"
marker_launch --skip-unless-marker guard-launch-monitor 0 >/dev/null
assert_eq "a reordered payload still keys on its session id" "skipped" "$(target_state)"
mkdir -p "$MARKER_DIR"
@@ -567,6 +573,57 @@ mkdir -p "$MARKER_DIR"
marker_launch --skip-unless-marker guard-launch-monitor 0 >/dev/null
assert_eq "a reordered payload finds its own marker" "ran" "$(target_state)"
+# --- a launch that can write no marker at all must not silence the monitor ---
+#
+# Both candidates fail on the launch side here, which leaves the Stop row
+# nothing to find. Reading only the marker FILE would then skip every Stop for
+# the rest of the session: a silent failure in the one detector that exists to
+# report silent failures. The Stop row therefore skips only when a candidate
+# DIRECTORY exists, and otherwise runs the monitor as it did before the flags.
+#
+# "Unwritable" is staged structurally rather than through permission bits, which
+# MSYS cannot set against Windows ACLs: `mkdir -p` refuses a parent that is a
+# regular FILE on every host, and refuses a target that already exists as one.
+# The marker root sits under a file; `TMPDIR` itself stays a real directory
+# (bash may place a here-string there) with a file occupying the name the tmp
+# fallback wants.
+BLOCKED_CASE="$PROBE_DIR/blocked-case"
+BLOCKED_TMP="$BLOCKED_CASE/tmp"
+mkdir -p "$BLOCKED_CASE" "$BLOCKED_TMP"
+BLOCKED_FILE="$BLOCKED_CASE/not-a-directory"
+: >"$BLOCKED_FILE"
+: >"$BLOCKED_TMP/disk-hygiene-guard-launch-monitor"
+BLOCKED_ROOT="$BLOCKED_FILE/data"
+
+blocked_launch() {
+ local flag="$1" subdir="$2" rc=0
+ rm -f "$MARKER_SEEN" "$MARKER_SEEN.argv"
+ printf '%s' "$MARKER_PAYLOAD" |
+ HOME="$MARKER_HOME" TMPDIR="$BLOCKED_TMP" \
+ bash "$FIXTURE_ROOT/hooks/run-python-hook.sh" \
+ --marker-root "$BLOCKED_ROOT" "$flag" "$subdir" \
+ "$MARKER_TARGET" "$MARKER_SEEN" 0 >/dev/null 2>&1 || rc=$?
+ printf '%s' "$rc"
+}
+
+MARKER_PAYLOAD='{"session_id":"sess-3","hook_event_name":"PreToolUse"}'
+blocked_rc="$(blocked_launch --launch-marker guard-launch-monitor)"
+assert_eq "a launch that can write no marker still runs its target" \
+ "ran" "$(target_state)"
+assert_eq "a launch that can write no marker reports its target's code" \
+ "0" "$blocked_rc"
+assert_eq "a failed marker write creates no candidate directory" "absent" \
+ "$([[ -d "$BLOCKED_ROOT" || -d "$BLOCKED_TMP/disk-hygiene-guard-launch-monitor" ]] &&
+ printf 'present' || printf 'absent')"
+assert_eq "and it leaves the blocking file alone" "file" \
+ "$([[ -f "$BLOCKED_FILE" ]] && printf 'file' || printf 'gone')"
+
+MARKER_PAYLOAD='{"session_id":"sess-3","hook_event_name":"Stop"}'
+blocked_rc="$(blocked_launch --skip-unless-marker guard-launch-monitor)"
+assert_eq "a Stop with no candidate marker directory runs the monitor" \
+ "ran" "$(target_state)"
+assert_eq "a Stop with no candidate marker directory exits 0" "0" "$blocked_rc"
+
# --- both wirings carry the flag the other one depends on ---
guard_rows="$(jq '[.hooks.PreToolUse[].hooks[] |
select(.command | contains("destructive_guard.py"))] | length' "$HOOKS_JSON")"