From 8d3ea153343e809114f363adb988ed6f5772e85e Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:42:51 -0400 Subject: [PATCH 1/4] fix(scripts): widen the drive-root litter detector to known temp-sink names The detector scored an empty C:\tmp zero: its only class was a single-letter drive-root directory naming a mounted drive, and 'tmp' is not a single letter. Add a second class for known temp-sink names at a drive root, the residue of a POSIX /tmp literal handed to a Windows-native consumer. The vocabulary is the sibling guard's (block-windows-drive-tmp.sh): tmp is the only drive-root sink it blocks, so tmp is the only name here, and the two lists grow together. The single-letter class and its mounted-drive precision rationale are untouched. The new class carries the same cwd-containment guard plus a DRIVE_ROOT_LITTER_IGNORE_SINKS opt-out for an operator who keeps a deliberate C:\tmp. Still advisory per ADR 0003: no measured precision, no required gate. Exit codes and the reported non-Windows no-op (still evaluated first) are unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Egdf11hXdBch1HTFmjB8FV --- docs/conventions/windows-path-emit/README.md | 26 ++++++-- scripts/check-drive-root-litter.sh | 59 +++++++++++++++-- scripts/check-drive-root-litter.test.sh | 68 +++++++++++++++++++- 3 files changed, 138 insertions(+), 15 deletions(-) diff --git a/docs/conventions/windows-path-emit/README.md b/docs/conventions/windows-path-emit/README.md index 55b2dd187..2327fbe18 100644 --- a/docs/conventions/windows-path-emit/README.md +++ b/docs/conventions/windows-path-emit/README.md @@ -158,13 +158,25 @@ every carrying plugin through a version bump for a function none of them calls. ## The detection net [`scripts/check-drive-root-litter.sh`](../../../scripts/check-drive-root-litter.sh) fails a host that -carries the defect's on-disk fingerprint: a directory at a drive root whose name is a single letter -that is **itself a mounted drive** on that host. Requiring the letter to name a real drive is what -keeps it precise — a one-character folder at a drive root is unremarkable on its own (`:\a` is -the workspace root on a GitHub-hosted Windows runner), and only becomes this defect's signature when -the letter is one an author could have spelled into an MSYS path. It also ignores a candidate that -contains the current working directory, so a checkout that genuinely lives under one is not called -residue. +carries the defect's on-disk fingerprint, in either of two classes: + +- **A single-letter directory naming a mounted drive.** A directory at a drive root whose name is a + single letter that is **itself a mounted drive** on that host — an MSYS `/d/...` literal resolved + against the current drive. Requiring the letter to name a real drive is what keeps it precise — a + one-character folder at a drive root is unremarkable on its own (`:\a` is the workspace root + on a GitHub-hosted Windows runner), and only becomes this defect's signature when the letter is one + an author could have spelled into an MSYS path. +- **A known temp-sink name at a drive root** (`C:\tmp`) — a POSIX `/tmp` literal resolved the same + way. The name vocabulary is deliberately the one + [`plugins/guardrails/hooks/block-windows-drive-tmp.sh`](../../../plugins/guardrails/hooks/block-windows-drive-tmp.sh) + already blocks: `tmp` is the only drive-root sink in that guard (`/var/tmp` and `%TEMP%` are + legitimate and never sit at a volume root), so it is the only name here; the two lists grow + together. An operator who keeps a deliberate `C:\tmp` exempts the name with + `DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp` — an env var rather than a marker file inside the directory, + because the detector cannot trust litter's own contents to prove intent. + +Both classes ignore a candidate that contains the current working directory, so a checkout that +genuinely lives under one is not called residue. Run it after any Windows verification pass: diff --git a/scripts/check-drive-root-litter.sh b/scripts/check-drive-root-litter.sh index a55d4fae8..4a3af0f60 100755 --- a/scripts/check-drive-root-litter.sh +++ b/scripts/check-drive-root-litter.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash -# Detect the on-disk fingerprint of an unconverted MSYS path handed to a -# Windows-native consumer: a directory sitting at a drive root whose name is a -# single letter that is ITSELF a mounted drive. +# Detect the on-disk fingerprint of an unconverted POSIX path handed to a +# Windows-native consumer. Two classes share the mechanism: +# * a directory at a drive root whose name is a single letter that is ITSELF +# a mounted drive (an MSYS /d/... literal), and +# * a directory at a drive root carrying a KNOWN TEMP-SINK NAME (a POSIX +# /tmp literal), e.g. C:\tmp. # # scripts/check-drive-root-litter.sh scan this host's drive roots # @@ -28,6 +31,22 @@ # command string); this is the same concern pointed at a producer we own, and it # looks at the filesystem AFTER a run rather than at a command before it. # +# The TEMP-SINK class is the same mechanism with the literal spelled `/tmp` +# instead of `//...`: Git Bash's real temp is a mount +# (`/tmp` -> `%TEMP%`), but a Windows-native consumer given the literal +# resolves it to `:\tmp` and creates it. The name vocabulary is +# deliberately the sibling guard's: `tmp` is the only drive-root sink +# block-windows-drive-tmp.sh blocks (`/var/tmp` and `%TEMP%` are legitimate and +# never sit at a volume root), so `tmp` is the only name here. Grow both lists +# together. Precision comes from the name being a sink nothing legitimately +# roots at a volume top on Windows - unlike the single-letter class there is no +# mounted-drive coincidence to require, so this class carries an opt-out: +# DRIVE_ROOT_LITTER_IGNORE_SINKS (space- or comma-separated names) exempts an +# operator who keeps a deliberate `C:\tmp`. An env var rather than a marker +# file inside the directory, because the detector cannot trust litter's own +# contents to prove intent, and the env var keeps the exemption visible at the +# invocation site. The single-letter class has no opt-out and is unaffected. +# # ADVISORY BY DEFAULT, not wired into a required lane that scans a live machine. # docs/adr/0003 is this repo's doctrine for that: a verification guard earns # default-on by measured precision, and this detector has none yet. CI runs its @@ -65,7 +84,7 @@ mount_root="${DRIVE_ROOT_LITTER_MOUNT_ROOT:-/}" mount_root="${mount_root%/}" # The set of mounted drive letters. This is both the set of roots to scan AND -# the set of directory names that count as a hit. +# the set of directory names that count as a single-letter-class hit. drives=() for letter in {a..z}; do [[ -d "$mount_root/$letter" ]] && drives+=("$letter") @@ -102,6 +121,30 @@ for drive in "${drives[@]}"; do done done +# Temp-sink class (see the header): a known sink name at a drive root. Same +# containment guard as above; DRIVE_ROOT_LITTER_IGNORE_SINKS exempts a name. +sink_names=(tmp) +ignored_sinks="${DRIVE_ROOT_LITTER_IGNORE_SINKS:-}" +ignored_sinks=" ${ignored_sinks//,/ } " +for drive in "${drives[@]}"; do + for name in "${sink_names[@]}"; do + [[ "$ignored_sinks" == *" $name "* ]] && continue + candidate="$mount_root/$drive/$name" + [[ -d "$candidate" ]] || continue + cand_real="$(cd "$candidate" 2>/dev/null && pwd -P)" + [[ -n "$cand_real" ]] || cand_real="$candidate" + case "$here/" in + "$cand_real"/*) continue ;; + *) ;; # the cwd is elsewhere: the candidate is a real hit + esac + if [[ "$mount_root" == "" ]]; then + hits+=("$candidate (${drive^}:\\${name}\\)") + else + hits+=("$candidate") + fi + done +done + if ((${#hits[@]} == 0)); then echo "check-drive-root-litter.sh: ${#drives[@]} drive root(s) scanned under '${mount_root:-/}'; no drive-root litter found." exit 0 @@ -113,9 +156,11 @@ for hit in "${hits[@]}"; do done cat >&2 <<'EOF' -Each path above is a directory at a drive root named for another mounted drive - -the fingerprint of an MSYS path literal (/d/...) handed to a Windows-native -consumer, which resolved it against the CURRENT drive's root instead. +Each path above carries this defect's fingerprint at a drive root: a directory +named for another mounted drive (an MSYS /d/... literal) or a known temp-sink +name such as tmp (a POSIX /tmp literal). Either way, a POSIX path was handed to +a Windows-native consumer, which resolved it against the CURRENT drive's root +instead of the intended location. The litter is the cheap part. Whatever wrote it wrote to a path it did not intend, so any run that produced it may have measured something other than what diff --git a/scripts/check-drive-root-litter.test.sh b/scripts/check-drive-root-litter.test.sh index 9c742f629..2800b6f6b 100755 --- a/scripts/check-drive-root-litter.test.sh +++ b/scripts/check-drive-root-litter.test.sh @@ -60,6 +60,18 @@ mkdir -p "$TMP/nodrives/usr" "$TMP/nodrives/opt" # checkout: a repo that genuinely lives under a single-letter drive-root folder. mkdir -p "$TMP/checkout/c/data" "$TMP/checkout/d/c/work/repo" +# sink: the temp-sink fingerprint — C:\tmp, a POSIX /tmp literal resolved +# against the current drive's root. The inner mktemp-named entry mirrors the +# real instance this class was added for. +mkdir -p "$TMP/sink/c/data" "$TMP/sink/d/repos" +mkdir -p "$TMP/sink/c/tmp/tmp.rSFIkHm5DO" + +# sinkcheckout: a repo that genuinely lives under a drive-root tmp folder. +mkdir -p "$TMP/sinkcheckout/c/data" "$TMP/sinkcheckout/c/tmp/work/repo" + +# both: single-letter litter AND a temp sink on the same host. +mkdir -p "$TMP/both/c/data" "$TMP/both/d/repos" "$TMP/both/c/d" "$TMP/both/d/tmp" + # --- 1. Non-Windows host: no-op, reported, exit 0 --------------------------- run linux-gnu - if ((RC == 0)) && grep -qi 'no-op on a non-Windows host' <<<"$OUT"; then @@ -165,7 +177,61 @@ else fail "exclusion suppressed too much: rc=$RC out=$OUT" fi -# --- 8. Arguments are a usage error ----------------------------------------- +# --- 8. Temp-sink class: a drive-root tmp fires ------------------------------ +run msys "$TMP/sink" +if ((RC == 1)) && grep -q "$TMP/sink/c/tmp" <<<"$OUT"; then + pass "a drive-root tmp directory fails and is named (exit 1)" +else + fail "drive-root tmp should fail with exit 1: rc=$RC out=$OUT" +fi +if ! grep -q "$TMP/sink/c/data" <<<"$OUT" && ! grep -q "$TMP/sink/d/repos" <<<"$OUT"; then + pass "ordinary non-sink directories at a drive root are not reported" +else + fail "non-sink directory reported as litter: $OUT" +fi + +# --- 9. Temp-sink opt-out ---------------------------------------------------- +OUT="$(OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/sink" DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp bash "$SUT" 2>&1)" +RC=$? +if ((RC == 0)) && grep -q 'no drive-root litter found' <<<"$OUT"; then + pass "DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp exempts a deliberate drive-root tmp" +else + fail "sink opt-out should pass: rc=$RC out=$OUT" +fi +# ... and the opt-out does not bleed into the single-letter class. +OUT="$(OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/litter" DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp bash "$SUT" 2>&1)" +RC=$? +if ((RC == 1)); then + pass "the sink opt-out leaves the single-letter class armed" +else + fail "sink opt-out suppressed the single-letter class: rc=$RC out=$OUT" +fi + +# --- 10. A drive-root tmp containing the cwd is a checkout, not litter -------- +OUT="$(cd "$TMP/sinkcheckout/c/tmp/work/repo" && OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/sinkcheckout" bash "$SUT" 2>&1)" +RC=$? +if ((RC == 0)) && grep -q 'no drive-root litter found' <<<"$OUT"; then + pass "a drive-root tmp containing the cwd is not reported as litter" +else + fail "sink cwd-ancestor exclusion failed: rc=$RC out=$OUT" +fi +# ... and the same tree IS litter from elsewhere. +run msys "$TMP/sinkcheckout" +if ((RC == 1)) && grep -q "$TMP/sinkcheckout/c/tmp" <<<"$OUT"; then + pass "the same tmp directory is litter when it does not contain the cwd" +else + fail "sink exclusion suppressed too much: rc=$RC out=$OUT" +fi + +# --- 11. Both classes report together ---------------------------------------- +run msys "$TMP/both" +if ((RC == 1)) && grep -q "$TMP/both/c/d" <<<"$OUT" && grep -q "$TMP/both/d/tmp" <<<"$OUT"; then + pass "single-letter and temp-sink hits are reported in one run" +else + fail "both classes should be reported together: rc=$RC out=$OUT" +fi + +# --- 12. Arguments are a usage error ----------------------------------------- run msys "$TMP/clean" --check if ((RC == 2)) && grep -q 'usage' <<<"$OUT"; then pass "an unexpected argument is a usage error (exit 2)" From 6bb15d71c4b0520dd229c7b8cb33ae1f7267528c Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:39:18 -0400 Subject: [PATCH 2/4] fix(scripts): match sink names case-insensitively and dedupe the candidate guard Windows filesystems fold case, so C:\TMP is C:\tmp - but the sink matcher probed the literal lowercase name, which only worked where the host filesystem folded case for it, and the test fixtures' filesystems do not. Enumerate the drive root's entries and compare lowercased instead, so an uppercase TMP is caught on any host, and lowercase the DRIVE_ROOT_LITTER_IGNORE_SINKS values so an operator who writes TMP is not silently unexempted. Both directions are test-asserted. Also factor the copy-pasted candidate check (existence, cwd-containment, hit recording) into one record_if_litter helper called by both classes, so a future edit cannot land in one copy and miss the other. Single-letter class behavior is unchanged and its tests still pass; no-op ordering, exit codes, and the advisory posture are untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Egdf11hXdBch1HTFmjB8FV --- docs/conventions/windows-path-emit/README.md | 3 +- scripts/check-drive-root-litter.sh | 78 +++++++++++--------- scripts/check-drive-root-litter.test.sh | 27 +++++++ 3 files changed, 74 insertions(+), 34 deletions(-) diff --git a/docs/conventions/windows-path-emit/README.md b/docs/conventions/windows-path-emit/README.md index 2327fbe18..3a36d5c96 100644 --- a/docs/conventions/windows-path-emit/README.md +++ b/docs/conventions/windows-path-emit/README.md @@ -171,7 +171,8 @@ carries the defect's on-disk fingerprint, in either of two classes: [`plugins/guardrails/hooks/block-windows-drive-tmp.sh`](../../../plugins/guardrails/hooks/block-windows-drive-tmp.sh) already blocks: `tmp` is the only drive-root sink in that guard (`/var/tmp` and `%TEMP%` are legitimate and never sit at a volume root), so it is the only name here; the two lists grow - together. An operator who keeps a deliberate `C:\tmp` exempts the name with + together. Sink names match case-insensitively (Windows filesystems fold case, so `C:\TMP` is + `C:\tmp`). An operator who keeps a deliberate `C:\tmp` exempts the name, in any casing, with `DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp` — an env var rather than a marker file inside the directory, because the detector cannot trust litter's own contents to prove intent. diff --git a/scripts/check-drive-root-litter.sh b/scripts/check-drive-root-litter.sh index 4a3af0f60..4cfea409e 100755 --- a/scripts/check-drive-root-litter.sh +++ b/scripts/check-drive-root-litter.sh @@ -42,7 +42,12 @@ # roots at a volume top on Windows - unlike the single-letter class there is no # mounted-drive coincidence to require, so this class carries an opt-out: # DRIVE_ROOT_LITTER_IGNORE_SINKS (space- or comma-separated names) exempts an -# operator who keeps a deliberate `C:\tmp`. An env var rather than a marker +# operator who keeps a deliberate `C:\tmp`. Sink names match CASE-INSENSITIVELY +# in both directions - Windows filesystems are case-insensitive, so `C:\TMP` +# and `C:\tmp` are one directory, and the detector enumerates drive-root +# entries rather than probing the literal lowercase name so the contract holds +# on the case-sensitive filesystems the test fixtures run on; the opt-out +# accepts any casing for the same reason. An env var rather than a marker # file inside the directory, because the detector cannot trust litter's own # contents to prove intent, and the env var keeps the exemption visible at the # invocation site. The single-letter class has no opt-out and is unaffected. @@ -103,45 +108,52 @@ fi here="$(pwd -P 2>/dev/null)" || here="$(pwd)" hits=() + +# Shared candidate check for both classes: skip a non-directory, skip a +# candidate that contains the cwd (a real checkout location, not litter - see +# the comment above `here`), record everything else as a hit. +record_if_litter() { + local drive="$1" name="$2" candidate cand_real + candidate="$mount_root/$drive/$name" + [[ -d "$candidate" ]] || return 0 + cand_real="$(cd "$candidate" 2>/dev/null && pwd -P)" + [[ -n "$cand_real" ]] || cand_real="$candidate" + case "$here/" in + "$cand_real"/*) return 0 ;; + *) ;; # the cwd is elsewhere: the candidate is a real hit + esac + if [[ "$mount_root" == "" ]]; then + hits+=("$candidate (${drive^}:\\${name}\\)") + else + hits+=("$candidate") + fi +} + +# Single-letter class: a drive-root directory named for another mounted drive. for drive in "${drives[@]}"; do for name in "${drives[@]}"; do - candidate="$mount_root/$drive/$name" - [[ -d "$candidate" ]] || continue - cand_real="$(cd "$candidate" 2>/dev/null && pwd -P)" - [[ -n "$cand_real" ]] || cand_real="$candidate" - case "$here/" in - "$cand_real"/*) continue ;; - *) ;; # the cwd is elsewhere: the candidate is a real hit - esac - if [[ "$mount_root" == "" ]]; then - hits+=("$candidate (${drive^}:\\${name}\\)") - else - hits+=("$candidate") - fi + record_if_litter "$drive" "$name" done done -# Temp-sink class (see the header): a known sink name at a drive root. Same -# containment guard as above; DRIVE_ROOT_LITTER_IGNORE_SINKS exempts a name. -sink_names=(tmp) +# Temp-sink class (see the header): a known sink name at a drive root, matched +# case-insensitively by ENUMERATING the drive root's entries - a lowercase +# probe would rely on the host filesystem folding case, which the test +# fixtures' filesystems do not. DRIVE_ROOT_LITTER_IGNORE_SINKS exempts a name, +# any casing. +sink_names=" tmp " ignored_sinks="${DRIVE_ROOT_LITTER_IGNORE_SINKS:-}" -ignored_sinks=" ${ignored_sinks//,/ } " +ignored_sinks="${ignored_sinks//,/ }" +ignored_sinks=" ${ignored_sinks,,} " for drive in "${drives[@]}"; do - for name in "${sink_names[@]}"; do - [[ "$ignored_sinks" == *" $name "* ]] && continue - candidate="$mount_root/$drive/$name" - [[ -d "$candidate" ]] || continue - cand_real="$(cd "$candidate" 2>/dev/null && pwd -P)" - [[ -n "$cand_real" ]] || cand_real="$candidate" - case "$here/" in - "$cand_real"/*) continue ;; - *) ;; # the cwd is elsewhere: the candidate is a real hit - esac - if [[ "$mount_root" == "" ]]; then - hits+=("$candidate (${drive^}:\\${name}\\)") - else - hits+=("$candidate") - fi + for entry in "$mount_root/$drive"/*/; do + [[ -d "$entry" ]] || continue + name="${entry%/}" + name="${name##*/}" + name_lc="${name,,}" + [[ "$sink_names" == *" $name_lc "* ]] || continue + [[ "$ignored_sinks" == *" $name_lc "* ]] && continue + record_if_litter "$drive" "$name" done done diff --git a/scripts/check-drive-root-litter.test.sh b/scripts/check-drive-root-litter.test.sh index 2800b6f6b..eb5e40c94 100755 --- a/scripts/check-drive-root-litter.test.sh +++ b/scripts/check-drive-root-litter.test.sh @@ -66,6 +66,11 @@ mkdir -p "$TMP/checkout/c/data" "$TMP/checkout/d/c/work/repo" mkdir -p "$TMP/sink/c/data" "$TMP/sink/d/repos" mkdir -p "$TMP/sink/c/tmp/tmp.rSFIkHm5DO" +# sinkupper: the same fingerprint in uppercase — Windows filesystems are +# case-insensitive, so C:\TMP is C:\tmp and must be caught even on the +# case-sensitive filesystem this fixture lives on. +mkdir -p "$TMP/sinkupper/c/data" "$TMP/sinkupper/d/repos" "$TMP/sinkupper/c/TMP" + # sinkcheckout: a repo that genuinely lives under a drive-root tmp folder. mkdir -p "$TMP/sinkcheckout/c/data" "$TMP/sinkcheckout/c/tmp/work/repo" @@ -189,6 +194,13 @@ if ! grep -q "$TMP/sink/c/data" <<<"$OUT" && ! grep -q "$TMP/sink/d/repos" <<<"$ else fail "non-sink directory reported as litter: $OUT" fi +# Windows filesystems fold case, so an uppercase TMP is the same sink. +run msys "$TMP/sinkupper" +if ((RC == 1)) && grep -q "$TMP/sinkupper/c/TMP" <<<"$OUT"; then + pass "an uppercase drive-root TMP is detected (case-insensitive match)" +else + fail "uppercase TMP should be detected: rc=$RC out=$OUT" +fi # --- 9. Temp-sink opt-out ---------------------------------------------------- OUT="$(OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/sink" DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp bash "$SUT" 2>&1)" @@ -198,6 +210,21 @@ if ((RC == 0)) && grep -q 'no drive-root litter found' <<<"$OUT"; then else fail "sink opt-out should pass: rc=$RC out=$OUT" fi +# ... in any casing, both of the opt-out value and of the directory. +OUT="$(OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/sink" DRIVE_ROOT_LITTER_IGNORE_SINKS=TMP bash "$SUT" 2>&1)" +RC=$? +if ((RC == 0)) && grep -q 'no drive-root litter found' <<<"$OUT"; then + pass "an uppercase opt-out value (TMP) exempts a lowercase tmp" +else + fail "uppercase opt-out should exempt: rc=$RC out=$OUT" +fi +OUT="$(OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/sinkupper" DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp bash "$SUT" 2>&1)" +RC=$? +if ((RC == 0)) && grep -q 'no drive-root litter found' <<<"$OUT"; then + pass "a lowercase opt-out value exempts an uppercase TMP directory" +else + fail "opt-out should exempt an uppercase directory: rc=$RC out=$OUT" +fi # ... and the opt-out does not bleed into the single-letter class. OUT="$(OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/litter" DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp bash "$SUT" 2>&1)" RC=$? From c798f0c3971908002fa60b494ecd3cc21caf4235 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:25:14 -0400 Subject: [PATCH 3/4] docs(scripts): note the bash-4+ case folds depend on sitting below the host gate ${var,,} is bash 4.0+ and macOS ships bash 3.2; the sink matcher is safe only because the non-Windows gate exits before those lines are ever reached. Record that ordering dependency where a future edit would otherwise move it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Egdf11hXdBch1HTFmjB8FV --- scripts/check-drive-root-litter.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/check-drive-root-litter.sh b/scripts/check-drive-root-litter.sh index 4cfea409e..07e384059 100755 --- a/scripts/check-drive-root-litter.sh +++ b/scripts/check-drive-root-litter.sh @@ -140,7 +140,9 @@ done # case-insensitively by ENUMERATING the drive root's entries - a lowercase # probe would rely on the host filesystem folding case, which the test # fixtures' filesystems do not. DRIVE_ROOT_LITTER_IGNORE_SINKS exempts a name, -# any casing. +# any casing. The ${var,,} case folds below are bash 4.0+; they only ever +# execute behind the Windows host gate above (Git Bash ships bash 5.x), so a +# macOS bash 3.2 exits at the gate before reaching them - keep them below it. sink_names=" tmp " ignored_sinks="${DRIVE_ROOT_LITTER_IGNORE_SINKS:-}" ignored_sinks="${ignored_sinks//,/ }" From 9929b5e3dac58735695d352cef64de04d73c84c7 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 31 Aug 2026 01:11:14 -0400 Subject: [PATCH 4/4] fix(scripts): make the litter test suite hermetic against the operator opt-out The suite exec'd the SUT without clearing DRIVE_ROOT_LITTER_IGNORE_SINKS - a documented, user-facing opt-out an operator may export persistently. Inherited into the ordinary-detection invocations it silently flips four sink assertions into false failures unrelated to the code under test, the silent-false-signal class f8ab565 closed elsewhere. run() and the two inline cwd invocations now set every env var the SUT reads explicitly (empty equals unset for both seams), the two opt-out tests keep setting a live value per-call, and the suite POISONS its own environment with DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp up front so a future unhermetic invocation fails loudly in CI rather than only on an operator's machine. Verified by running the suite with the variable unset and with it exported: 26/26 both ways, identical assertion output. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Egdf11hXdBch1HTFmjB8FV --- scripts/check-drive-root-litter.test.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/scripts/check-drive-root-litter.test.sh b/scripts/check-drive-root-litter.test.sh index eb5e40c94..31bae434d 100755 --- a/scripts/check-drive-root-litter.test.sh +++ b/scripts/check-drive-root-litter.test.sh @@ -27,17 +27,31 @@ trap 'rm -rf "$TMP"' EXIT # run [args...] — sets OUT (stdout+stderr) and RC. # Deliberately NOT called through a command substitution: that would fork a # subshell and the exit code would never reach the caller. +# +# HERMETIC against the invoking environment: every env var the SUT reads is set +# explicitly here. DRIVE_ROOT_LITTER_IGNORE_SINKS is a documented operator +# opt-out an operator may export in a shell profile; inherited into these +# invocations it would silently flip the sink-detection assertions into false +# failures unrelated to the code under test. Empty is equivalent to unset for +# both seams (the SUT reads them with ${...:-}). The two opt-out tests set a +# live value per-call, on top of this baseline. run() { local ostype="$1" root="$2" shift 2 if [[ "$root" == "-" ]]; then - OUT="$(OSTYPE="$ostype" bash "$SUT" "$@" 2>&1)" + OUT="$(OSTYPE="$ostype" DRIVE_ROOT_LITTER_MOUNT_ROOT='' DRIVE_ROOT_LITTER_IGNORE_SINKS='' bash "$SUT" "$@" 2>&1)" else - OUT="$(OSTYPE="$ostype" DRIVE_ROOT_LITTER_MOUNT_ROOT="$root" bash "$SUT" "$@" 2>&1)" + OUT="$(OSTYPE="$ostype" DRIVE_ROOT_LITTER_MOUNT_ROOT="$root" DRIVE_ROOT_LITTER_IGNORE_SINKS='' bash "$SUT" "$@" 2>&1)" fi RC=$? } +# Poison the outer environment with the opt-out so the hermeticity above is a +# STANDING assertion, not a comment: if a future invocation forgets to clear +# DRIVE_ROOT_LITTER_IGNORE_SINKS, the sink-detection tests fail loudly right +# here in CI instead of only on an operator's machine. +export DRIVE_ROOT_LITTER_IGNORE_SINKS=tmp + # --- Fixtures --------------------------------------------------------------- # A "mount root" is what Git Bash exposes as `/`: every mounted drive appears as # a single-letter directory under it. @@ -166,7 +180,7 @@ else fi # --- 7. A candidate containing the cwd is a checkout, not litter ------------- -OUT="$(cd "$TMP/checkout/d/c/work/repo" && OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/checkout" bash "$SUT" 2>&1)" +OUT="$(cd "$TMP/checkout/d/c/work/repo" && OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/checkout" DRIVE_ROOT_LITTER_IGNORE_SINKS='' bash "$SUT" 2>&1)" RC=$? if ((RC == 0)) && grep -q 'no drive-root litter found' <<<"$OUT"; then pass "a drive-root directory containing the cwd is not reported as litter" @@ -235,7 +249,7 @@ else fi # --- 10. A drive-root tmp containing the cwd is a checkout, not litter -------- -OUT="$(cd "$TMP/sinkcheckout/c/tmp/work/repo" && OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/sinkcheckout" bash "$SUT" 2>&1)" +OUT="$(cd "$TMP/sinkcheckout/c/tmp/work/repo" && OSTYPE=msys DRIVE_ROOT_LITTER_MOUNT_ROOT="$TMP/sinkcheckout" DRIVE_ROOT_LITTER_IGNORE_SINKS='' bash "$SUT" 2>&1)" RC=$? if ((RC == 0)) && grep -q 'no drive-root litter found' <<<"$OUT"; then pass "a drive-root tmp containing the cwd is not reported as litter"