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