All three were found by review and reproduced before fixing. Each let a run
report success while proving nothing -- the shape this harness exists to
prevent.
A reused prefix defeated dropped-write detection entirely. Run 1's files were
still there when run 2's manifest was taken, so the manifest reported writes
that never happened and the fixture pair diffed CLEAN. The doc's own examples
demonstrated that workflow. A non-empty prefix is now refused -- not deleted,
since the path is user-supplied -- and the examples use distinct prefixes.
A missing bwrap was swallowed. trace_prefix_bwrap_argv returned 1, but the
caller reads it through `mapfile < <(...)` and a process substitution's status
is discarded, so runner ended up empty and the run continued with no sandbox
and no uid 0. Same swallow as the grep-status one fixed earlier, one layer up.
bwrap is now probed in validate_requirements, before anything runs.
The absolute-path scan only fired when no --bind-abs was given at all, so
binding one path and missing three produced no warning. It now runs whenever
--prefix is given and reports only what is not already covered. Fixing that
surfaced a second defect: --bind-abs on a FILE aborted with "Can't mkdir
/etc/profile: Not a directory", and binding its parent reintroduced the
wholesale /etc shadowing this change had already measured as harmful ($SUDO_USER
collapsing to /home//, the real /etc/profile unreadable). Files are now bound
file-over-file, seeded with a copy so a script that greps before appending sees
real content.
Also measured rather than argued: the XDG redirect. A new fixture run with a
real XDG_DATA_HOME pointing at a decoy shows all four writes landing in the
prefix and the decoy empty -- the rsync --delete data-loss path, proven closed
without running install_leechblock.sh. And no-prefix output was diffed against
cdf2a55 from a detached worktree: byte-identical, so existing baselines hold.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Shell redirections are builtins, so no PATH stub can intercept
`cat > /etc/modprobe.d/x.conf`. That left installer scripts unverifiable:
stub cp/chmod/ln and the trace is nearly empty, don't and the run rewrites
the live system.
--prefix redirects HOME and every XDG_* base into a temp tree. XDG_DATA_HOME
is exported explicitly, not left to default off HOME: install_leechblock.sh
reads it directly and then runs `rsync -a --delete` at it.
--bind-abs binds hardcoded absolutes via bwrap --unshare-user --uid 0, leaf
directories only. uid 0 is load-bearing beyond permissions: common.sh's
require_root does `exec sudo "$0"`, and under the sudo stub that truncated
the whole run to one recorded call -- a trace that diffs clean against any
split. Binding /etc wholesale was measured to shadow /etc/passwd, collapse
$SUDO_USER to /home//, and mask a nested bind, so the scanner never emits it.
The trace gains a manifest of every file written, with content hashes. That
section is the point: with it, a dropped write in the fixture pair is a
one-line diff; with it stripped, the two traces are identical, i.e. the old
harness passed that broken split. Hashes normalize the prefix to @PREFIX@
because install_usage_monitoring.sh writes an unquoted heredoc that bakes
$HOME into the artifact.
Verified: ~/.config/systemd/user, ~/.local/bin, ~/.local/share and
/etc/modprobe.d byte-identical before and after; usage-report units still 2.
The three prior regressions are now committed fixtures rather than ad-hoc --
one was silently neutered mid-change when a trailing echo added for SC2034
became the function's last statement and turned exit 1 into exit 0.
Stub generation moved to lib/ because --prefix pushed the harness to 252
lines, over the cap it exists to serve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The previous commit broke `./lint_python.sh` from the repo root. That path
is a symlink into meta/, and `dirname` does not resolve symlinks, so
SCRIPT_DIR became the repo root and the new `source "$SCRIPT_DIR/lib/..."`
line pointed at a lib/ that does not exist -- an instant exit under
set -euo pipefail.
The bug was latent before the split: the same unresolved SCRIPT_DIR only
fed PROJECT_ROOT, so a root-path run degraded quietly. Adding a source
line is what turned a wrong value into a hard failure.
It was invisible because every check I ran used `bash meta/lint_python.sh`
-- the one entry point that could not fail. Both paths are now verified:
`./lint_python.sh` exits 0 through the symlink, and the direct invocation
still diffs byte-identical against the pre-split baseline. No CI job, hook
or wrapper calls it, so nothing automated was broken in the interim.
Recorded in docs/shell-split-verification.md as its own trap, with the
commands to enumerate a script's other entry points before splitting it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
A stub that prints nothing makes `size=$(du -sk x)` empty, so every
`((size > 0))` is false and the trace walks past every guarded branch.
Two such traces compare identical while exercising none of the changed
code -- a green check that proves nothing, which is the exact failure
mode this harness exists to prevent.
--stub now takes `name=output`. Measured on a fixture whose `sudo rm -rf`
is guarded by a size test: the silent stub gives `size=[]` / "branch
skipped" and never records the rm; `--stub 'du=4096\t/some/dir'` gives
`size=[4096]` / "BRANCH TAKEN" and captures it.
This weakens the earlier disk_cleanup_check.sh trace, which used a silent
du stub: that trace only proved the empty-size path identical. The real
run from /tmp is what covered the populated path. Noted in the evidence.
Docs also record two limits found while surveying: file-placement
installers are not yet traceable (stub cp/chmod/ln and the trace is
empty; don't and it rewrites the live system), and baselines for scripts
reading relative paths must run from the script's own directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Adding it to the handoff pushed that file to 280 lines -- the handoff is
not exempt from the cap it exists to enforce. The usage, the two caveats
(stubs always exit 0; unstubbed binaries run for real) and the proof it
catches both bug classes now live in docs/shell-split-verification.md,
which is the file dedicated to exactly this. The handoff keeps a pointer.
Also dropped the per-directory count table, which carried a "do not work
from this table" warning and went stale every session, and compressed the
co-agent narrative to the diagnostic commands worth keeping. 280 -> 246.
Recorded there too: the harness needs no main(), which matters because
none of the remaining near-miss shell targets have one, so
verify_shell_split.sh cannot check any of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The prettier hook is pre-push only, so this only surfaced when the push
ran. Two list-continuation lines rewrapped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Three new sections, each earned this session:
- A --skip-install flag is not evidence a script is safe to run.
shell_check.sh gates only the pacman branch on it; the AUR branch runs
regardless, and a "baseline" run installed four packages before I
killed it. Grep the flag's variable and read every hit first.
- A stubbed run cannot reach a bug in a seam that passes state. It
sources the libs and never calls them. analyze_repo.sh passed bash -n,
shellcheck, both diffs and the stub, and still aborted at runtime --
set -e turning a function's last conditional into its return value,
and a self-referencing nameref warning into the script's own output.
- Do not make a clean file shfmt-dirty, and re-count comment banners
after shfmt -w, which deletes ones it thinks are stray.
Adding them pushed the doc to 285, so it is split: recipes.md keeps how
to make a split (231), the new verification.md holds how to find out it
is wrong (70). The recipe doc now points at it from the stubbed-run
bullet, where someone about to trust the stub will actually read it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY