Record the unstubbed pkill hazard; retarget the brief at leechblock
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

Probed the two remaining candidates at HEAD instead of guessing:

nvidia_troubleshoot.sh is ruled OUT. Its trace dies at step 3 because
backup_file writes /etc/profile.backup.<stamp>, a new SIBLING in unbound /etc
— a file bind cannot cover siblings and bare /etc is refused. 239 of its 336
lines never execute, so keeping the unproven ones in the entry (the rule the
pacman split established) leaves the entry at ~265, still over the cap. The
file cannot satisfy both the rule and the goal.

install_leechblock.sh is the best-conditioned target left: exit 0, 93 files
written, and all 93 hashes byte-identical across two consecutive runs. Only
the echoed prefix path and curl's progress meter vary.

But it fires an unstubbed `pkill` at every Chromium-family browser. pkill is
not in the stub set and --prefix cannot contain it, because signals are not
filesystem writes. Two probe runs fired it, harmless only because no browser
happened to be open. That is now documented as its own trap, and the brief
tells the next session to pass --stub pkill and check for a live victim first.

Also recorded: for a target embedding $(date), diff the written files' CONTENTS
with the date normalized rather than falling back to comparing sizes — that
still catches a dropped line, which a size comparison does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-17 07:50:00 +02:00
parent 4eedc17b17
commit 4e016622b6
2 changed files with 177 additions and 205 deletions

View File

@ -1,4 +1,4 @@
# Next session: split `install_pacman_wrapper.sh`
# Next session: split `install_leechblock.sh`
> **Paste this whole file into a fresh Claude session opened in `~/testsAndMisc`.**
> It is self-contained. Do not go looking for the previous session's context.
@ -6,225 +6,197 @@
## The one job
Split
`linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh`
(316 lines, over the 250-line cap by 66) into files that are each under the
`linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh`
(485 lines, over the 250-line cap by 235) into files that are each under the
cap, and **prove by execution** that it still does exactly the same thing.
The harness already exists and has already been run against this exact file —
its baseline is captured and its one blind spot is mapped below. Use it, don't
rebuild it.
This is the best-conditioned target left: it is the only remaining one that
traces **to completion with exit 0**, and its 93 written-file hashes are
identical across runs. Read the safety note below before you run it, though —
it kills browsers.
## What landed last session (do not redo)
## What landed in the last session (do not redo)
`install_usage_monitoring.sh` (290 lines) is done, committed and pushed as
`26965ba`: a 53-line entry plus five libs in a sibling `lib/`, with an empty
before/after trace diff including all five generated-file hashes. That split is
the worked example — read it before starting this one, the shape transfers.
Two splits, both committed and pushed, both proven by an empty before/after
trace diff:
Two things it discovered that this file inherits:
- `install_usage_monitoring.sh` 290 → 53-line entry + 5 libs (`26965ba`)
- `install_pacman_wrapper.sh` 316 → 128-line entry + 4 libs (`4eedc17`)
- The pre-commit hook runs **`shfmt -w` on every staged shell file** and
re-stages it. It rewrites `cat > "x" << 'EOF'` to `cat >"x" <<'EOF'`. If any
test greps a script's source text for a redirection, that rewrite silently
breaks extraction. Run `shfmt -w` on your files yourself, then re-verify, so
the state you tested is the state that gets committed.
- Repo-wide `jscpd` currently reports **2.51% (over the 2% gate)** from the
working tree, but every clone above the HEAD baseline is vendored
`.venv`/`.ci-mirror-venv` site-packages (`tqdm/completion.sh`,
`virtualenv/activate.sh`). Measured at HEAD in a clean worktree it is 1.47%
with 27 clones, and the committed hook run passed. Don't chase it, and don't
"fix" it by touching vendored files.
Read the `4eedc17` diff first — it is the closest worked example, and three of
its lessons apply directly here:
## The baseline you must reproduce
- **Keep path/variable definitions in the ENTRY script, not a `lib/paths.sh`.**
A definitions-only lib assigns without referencing, which is SC2034 on every
variable, and suppressions are forbidden. Libs that only _reference_ globals
are fine. `export` silences it but leaks the variables into every child
process — a behaviour change the trace cannot see. Don't.
- **The pre-commit hook runs `shfmt -w` on staged shell files** and re-stages
them, rewriting `cat > "x" << 'EOF'` into `cat >"x" <<'EOF'`. Run `shfmt -w`
yourself first, then verify, so the tested state is the committed state.
- **Enumerate every test that greps the target's source text before designing
the seams.** The pacman split had eight such assertions across two test libs;
one (`chattr +i`) was missed on the first pass and failed loudly. Run:
```bash
grep -rn 'install_leechblock' linux_configuration/tests/ meta/ .github/
```
Captured at commit `26965ba`. **Verified deterministic**: two consecutive runs
at HEAD produced byte-identical traces, so a before/after diff is real proof.
Repo-wide `jscpd` reports 2.51% from the working tree but 1.47% at HEAD in a
clean worktree — the excess is entirely vendored `.venv`/`.ci-mirror-venv`
site-packages. Don't chase it; measure in a worktree if you need a real number.
## SAFETY: this script kills browsers and deletes directories
Two hazards, both measured:
1. **Line ~217 runs
`pkill -f 'google-chrome|chromium|brave-browser|vivaldi|thorium'`.** `pkill`
is NOT in the harness stub set, and `--prefix` cannot contain it — signals
are not filesystem writes. Tracing this script closes every open browser.
Check nothing is running (`pgrep -af 'chrome|chromium|thorium'`) and pass
`--stub pkill` before any run.
2. **It runs `rsync -a --delete` at `$XDG_DATA_HOME/leechblockng`.** This is
safe under `--prefix` — the harness exports `XDG_DATA_HOME` into the prefix
explicitly _because of this script_ — but never run it outside the harness.
Verified after the probe runs: `~/.local/share/leechblockng/` untouched (still
dated Aug 9), no Chrome profile file modified, `/usr/local/bin/browser-preexec-wrapper`
untouched. The node seeder resolves profiles via `os.homedir()`, which the
prefix redirects, so it found no profiles and wrote nothing ("Seeding" appears
zero times in the trace).
## The baseline
```bash
F=linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh
F=linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh
rm -rf /tmp/pfx-before # fresh, NOT emptied — the harness refuses a non-empty prefix
rm -rf /tmp/pfx-before # fresh, NOT emptied — the harness refuses a non-empty prefix
meta/scripts/trace_shell_split.sh "$F" --prefix /tmp/pfx-before \
--bind-abs /usr/local/bin \
--bind-abs /usr/local/share/digital_wellbeing \
--bind-abs /usr/local/share/digital_wellbeing/virtualbox \
--bind-abs /var/lib/pacman-wrapper \
--out /tmp/before.txt
--stub pkill --out /tmp/before.txt
```
Expect **exit status 1** (see the blind spot below — this is correct, not a
failure), one stubbed call (`npm install --prefix …`), and 16 files written:
Expect **exit 0** and **93 files written**. Note the probe baseline was
captured _without_ `--stub pkill`; adding the stub is correct and may add one
line to the stubbed-call list, so capture your own baseline with the same flags
you will use afterwards.
```
abs/usr/local/bin/heavy_job_lock.sh size=7221 sha=3961b14e6a6a8b86
abs/usr/local/bin/makepkg_capped size=2037 sha=3acf1189e93e5771
abs/usr/local/bin/mkpkg size=178 sha=da4a2fa3e8f5b6ab
abs/usr/local/bin/pacman_blocked_keywords.txt size=1042 sha=7bd951741c649b60
abs/usr/local/bin/pacman_greylist.txt size=174 sha=2dc47dd4a25d6aca
abs/usr/local/bin/pacman_lock_lib.sh size=6581 sha=147ea7a163000f82
abs/usr/local/bin/pacman_whitelist.txt size=6079 sha=cfec8efc4a2330c1
abs/usr/local/bin/pacman_wrapper size=29410 sha=548fc2b53ee4cea6
abs/usr/local/bin/words.txt size=1375483 sha=f5c9d52b244f9973
abs/usr/local/share/digital_wellbeing/install_leechblock.sh size=15965 sha=e4ec4b37fd9113a3
abs/usr/local/share/digital_wellbeing/leechblock_defaults.json size=51084 sha=ca068b74282921c6
abs/usr/local/share/digital_wellbeing/package.json size=258 sha=618fa688d2edfa4f
abs/usr/local/share/digital_wellbeing/seed_leechblock_storage.js size=4176 sha=1611cb17124fd388
abs/usr/local/share/digital_wellbeing/virtualbox/enforce_vbox_hosts.sh size=12601 sha=ab039aedc3cd84cd
abs/var/lib/pacman-wrapper/policy.sha256 size=412 sha=0aa8d95524660324
abs/var/lib/pacman-wrapper/source.sha256 size=1374 sha=4c928a499d437070
**Two things vary between runs and are NOT split defects** — normalize them
before diffing:
- The prefix path is echoed inside the six `sudo sed -i …--load-extension=…`
stubbed calls.
- `curl`'s progress meter (download speeds/times) in stderr.
Everything else is stable: all 93 content hashes were byte-identical across two
consecutive HEAD runs. The manifest section alone is the real assertion:
```bash
sed -n '/=== files written/,/=== stdout/p' /tmp/before.txt > /tmp/before.man
sed -n '/=== files written/,/=== stdout/p' /tmp/after.txt > /tmp/after.man
diff /tmp/before.man /tmp/after.man # must be empty
```
The last stderr line is
`ln: failed to create symbolic link '/usr/bin/pacman': Permission denied`.
Capture the baseline at HEAD **before editing anything**, then diff your split
against it. Unlike last file, a detached worktree is fine here — nothing
interpolates the repo path into a generated artifact (verified: the drift
manifest hashes source files by `readlink -f`, and those hashes are of file
_contents_, not paths).
## The one blind spot — this is the whole design constraint
`--bind-abs /usr/bin` **cannot be used**: it mounts an empty overlay over the
directory the sandbox needs to exec anything, and the run dies with
`bwrap: execvp bash: No such file or directory` before reaching the script.
Measured last session; it is a hard limit of the harness.
So the trace reaches **line 304** (`ln -sf "$WRAPPER_DEST" /usr/bin/pacman`),
hits EPERM, and exits 1. Lines 304 and 306 (the final `echo`) are the only
statements that never execute — everything before them is covered, 16 files
written and 19 stdout lines deep.
**Therefore: keep lines 304–316 in the entry script.** Anything you move into a
lib gets proven by the trace; anything at or after 304 does not, and a broken
split there would diff clean. This is a constraint on where the seams go, not a
reason to skip the file.
Note the absolute-path scanner does **not** list `/usr/bin` when it refuses to
run — it follows variables into `cat >`/`cp` and misses the literal `ln -sf`
target. Don't read its list as complete.
Also diff the full traces with the two varying patterns normalized, so a change
in stdout ordering or a dropped step still shows.
## Hazards specific to THIS file
### 1. It self-sudos at line 9
### 1. `SCRIPT_DIR` at line ~205 finds sibling data files
```bash
if [ "$EUID" -ne 0 ]; then
sudo "$0" "$@"
exit $?
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULTS_SRC="$SCRIPT_DIR/leechblock_defaults.json"
```
Under `--bind-abs` the run is already uid 0 (bwrap `--unshare-user --uid 0`),
so this branch is skipped and the script proceeds for real — that is why the
trace is 16 files deep rather than the three-line `require_root` truncation.
If you move this block, `$0` must still be the entry point.
It also locates `seed_leechblock_storage.js` and `node_modules/`. In a sourced
lib `${BASH_SOURCE[0]}` resolves to `lib/`, so these would silently point at
`lib/leechblock_defaults.json` and the defaults would be skipped with only a
warning — exit status unchanged. Compute it once in the entry and let libs read
it, exactly as `REPO_DIR` was handled in `26965ba`.
### 2. `source.sha256` is a drift manifest — but a split does not disturb it
### 2. Network downloads make timing non-deterministic
Line ~247 hashes seven **source** files (`WRAPPER_SOURCE`, `LOCK_LIB_SOURCE`,
`BLOCKED_SOURCE`, `GREYLIST_SOURCE`, `MAKEPKG_CAPPED_SOURCE`, `MKPKG_SOURCE`,
`WHITELIST_SOURCE`) plus the installed lock lib. Checked: it does **not** hash
the installer itself, and there is no glob that would pick up a new `lib/`.
So `sha=4c928a499d437070` should stay put across a split — if it moves, you
changed one of those seven copied files, which is a real regression.
`curl` fetches the LeechBlockNG tarball and jQuery UI. Content is stable (same
hashes across runs) but progress output is not. Do not `--stub curl` — that
would skip the download and empty out most of the 93 files.
`check_and_enable_services.sh` replays this manifest with `sha256sum -c` from
systemd with `cwd=/`, which is why the entries must be absolute
(`readlink -f`). Don't make those paths relative while tidying.
### 3. A `trap 'rm -rf "$tmpdir"' EXIT` is set mid-script
### 3. `chattr +i` / immutability
The script makes policy files immutable and has unlock/relock helpers
(`is_immutable_file`, `unlock_immutable_file_if_needed`,
`relock_files_on_exit`, lines 58–84) wired to a trap. Inside the sandbox these
warn ("Could not make integrity file immutable") and continue — that warning is
in the baseline and is expected. If you move the trap or the helpers, check the
trap still fires from the entry script.
### 4. It writes a _live_ system when run for real
`/usr/bin/pacman` is currently a symlink to `/usr/local/bin/pacman_wrapper`.
The sandbox protects this (the `ln` is denied — that denial is the positive
proof the bind worked), but **never run this script outside the harness** to
"check something".
Set inside the download branch (~line 143). If you move that branch into a lib,
the trap still registers globally, but check it is not overwritten by another
`trap … EXIT` you introduce.
## Rules that will bite you
- **`SCRIPT_DIR` must resolve symlinks**:
`SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"`. The
`SOURCE_*` vars are built from `dirname "$0"` and the code comments say so
explicitly — if you move that computation into a lib, pass the value in
rather than recomputing it at a different directory depth.
- **`SCRIPT_DIR` must resolve symlinks** in the entry:
`SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"`.
- **Wrapping top-level code in a function changes what `set -e` sees.** A bare
`((x > 0)) && FLAG=true` as a function's last statement becomes its return
value; if false, the script dies. Check the last statement of every function
you create; append `|| true` if it is a bare conditional.
`[[ -n $x ]] && arr+=("$x")` as a function's last statement becomes its
return value; append `|| true` if so.
- **No suppressions.** No `# shellcheck disable`, no per-file ignores.
- New `.sh` files need the executable bit, a `#!/bin/bash` shebang, and **no
- New `.sh` libs need the executable bit, a `#!/bin/bash` shebang, and **no
`set -euo pipefail`** (a sourced lib inherits the caller's strict mode).
Convention: a sibling `lib/` directory; see the `bin/lib/` that landed in
`26965ba`.
- **Verify with `pre-commit run shellcheck --files …`, not a bare
`shellcheck -x`.** The latter follows `source=` directives and hides SC2034
in a standalone lib; the former is the real gate and does not.
- Every commit touching code needs an evidence JSON in
`docs/superpowers/evidence/`; **≥4 staged code files** additionally needs a
fresh `docs/superpowers/contracts/*.json`. Validate both with
`python3 meta/scripts/validate_contract.py` /
`meta/scripts/validate_evidence.py` before committing.
- Markdown needs `npx prettier --write` — prettier is pre-push only, so a file
that passes every per-commit gate can still fail the push.
`meta/scripts/validate_evidence.py`.
- Markdown needs `npx prettier --write` — prettier is pre-push only.
- Work directly on `main`; commit and push. `git stash` and branch creation are
blocked by hooks.
- `git push` runs `ci-mirror` (clean-venv install + `pre-commit --all-files` +
pytest) and takes minutes. Never edit files while a push is running.
- `git push` runs `ci-mirror` and takes minutes. Never edit files while a push
is running.
- **Do not wire the file-length pre-commit hook.** It must land last, only once
`bash ~/utils/scripts/check_file_length.sh --all` exits 0, or every push
fails.
`bash ~/utils/scripts/check_file_length.sh --all` exits 0.
## Read these first
1. `docs/shell-split-verification.md` — how a green split is still broken. The
`--prefix` section covers the manifest, the empty-prefix rule, the
`require_root` truncation trap, and the `/usr/bin` limit above.
1. `docs/shell-split-verification.md` — how a green split is still broken. Now
covers the `/usr/bin` bind limit, the unstubbed `pkill`, and how to compare
when the target embeds a timestamp.
2. `docs/shell-split-recipes.md` — how to actually make a split.
3. The `26965ba` diff — the worked example of the entry+lib shape.
3. The `4eedc17` and `26965ba` diffs — two worked examples of the entry+lib
shape.
## Definition of done
- `install_pacman_wrapper.sh` and every file split out of it are **under 250
lines** (`bash ~/utils/scripts/check_file_length.sh --all` no longer lists
any of them).
- `diff /tmp/before.txt /tmp/after.txt` is **empty** — same exit status (1),
same stubbed call, and all 16 content hashes identical.
- Nothing you moved into a lib sits at or after line 304.
- `shellcheck` clean and `shfmt` clean on every touched file; zero
suppressions.
- `/usr/bin/pacman` still symlinks to `/usr/local/bin/pacman_wrapper` and
`pacman --version` still works, after every verification run.
- `install_leechblock.sh` and every file split out of it are **under 250
lines**.
- The trace manifest (93 files, all hashes) is identical before and after, and
the normalized full traces are identical, both at exit 0.
- Any test that greps the installer's source text still passes, with no
assertion weakened.
- `pre-commit run shellcheck` clean, `shfmt` clean, zero suppressions.
- `~/.local/share/leechblockng/` and all Chrome profile dirs unmodified by the
verification runs — check mtimes before and after.
- Committed and pushed, with evidence (and a contract if ≥4 code files).
## If there is time
## Ruled out — do not attempt these
Take the next target by the same method. Pick by **verifiability**, not by line
count — a big file you can run beats a small one you cannot:
- **`nvidia_troubleshoot.sh` (336).** Measured this session: its trace dies at
step 3 because `backup_file` writes `/etc/profile.backup.<stamp>`, a _new
sibling_ in unbound `/etc` (a file bind cannot cover siblings, and bare
`/etc` is refused). 239 of its 336 lines never execute. Keeping the unproven
ones in the entry — the rule the pacman split established — leaves the entry
at ~265, still over the cap. It cannot satisfy both the rule and the goal.
- **`block_compulsive_opening.sh` (705)** — `install_all` copies the running
script into `/usr/local/bin`; an entry+lib shape ships an entry whose
`SCRIPT_DIR` has no `lib/`, breaking three daily-use apps plus the pacman
rewrap hook.
- Blocked on NAMED blockers, not line count:
`check_and_enable_services.sh` (1337), `steam_compatibility.sh` (663),
`libre_translate.sh` (488), `enforce_vbox_hosts.sh` (443). See
`refactor_claude_todo_resume.md`.
```bash
bash ~/utils/scripts/check_file_length.sh --all
```
`setup_thorium_startup.sh` (443) is unprobed — trace it at HEAD before
committing to it, the same way this one was probed.
Remaining newly-unblocked targets, ascending difficulty: `nvidia_troubleshoot.sh`
(336, needs `--bind-abs /etc/modprobe.d --bind-abs /etc/X11 --bind-abs
/etc/profile`; it embeds `$(date)` in its config so that one hash varies every
run — compare file list and sizes instead), `setup_thorium_startup.sh` (443),
`install_leechblock.sh` (485 — confirm where `INSTALL_ROOT`/`VERSION_DIR` point
BEFORE the first run, it uses `rsync -a --delete`).
## Known pre-existing failure (not yours)
**Still out of scope:** `block_compulsive_opening.sh` (705) — `install_all`
copies the running script into `/usr/local/bin`, and an entry+lib shape ships
an entry whose `SCRIPT_DIR` has no `lib/`, breaking three daily-use apps plus
the pacman rewrap hook. Leave it.
Also still blocked on a NAMED blocker, not on line count — do not "just split"
these: `check_and_enable_services.sh` (1337, every `check_*` writes one
`SERVICE_STATUS`), `steam_compatibility.sh` (663), `libre_translate.sh` (488,
~19 globals cross any seam), `enforce_vbox_hosts.sh` (443, every seam falls
inside a heredoc). See `refactor_claude_todo_resume.md`.
`bash linux_configuration/tests/test_security_hardening.sh` exits 1 at HEAD
with exactly one failure, `❌ FAIL: Compulsive block wrappers installed`. It
belongs to `block_compulsive_opening.sh`. Capture it before and after your
change and confirm it is unchanged; do not try to fix it.

View File

@ -12,10 +12,8 @@ mutation.
`shell_check.sh` gates only `install_if_missing`'s pacman branch on
`SKIP_INSTALL`. The AUR branch inside `install_linters` runs regardless, so a
backgrounded "baseline" run installed four packages (`python-pbr`,
`python-fixtures`, `python-discover`, `python-reno`) before it was killed
mid-`makepkg`. Decision 6 was violated by a flag that looked like it granted
permission.
backgrounded "baseline" run installed four packages before it was killed
mid-`makepkg`. Decision 6 was violated by a flag that looked like permission.
Before running any script to capture a baseline, grep the flag's variable and
confirm it gates **every** mutation path, not just the first one:
@ -69,21 +67,19 @@ diff /tmp/before.txt /tmp/after.txt
```
It was verified against both bugs above: the `set -e` function tail shows up as
exit 1 with the later calls missing from the trace, and the nameref shows up as
`circular name reference` in stderr despite exit 0 and correct stdout. A run
against a fixture calling `systemctl` left the real unit untouched.
exit 1 with later calls missing, and the nameref as `circular name reference`
in stderr despite exit 0. A fixture calling `systemctl` left the unit untouched.
Three things to watch:
Four things to watch:
- **A silent stub produces an identical, meaningless trace.** This is the
trap. `du` stubbed to print nothing makes `size=$(du -sk x)` empty, so every
`((size > 0))` is false, every guarded branch is skipped, and two such traces
match perfectly while exercising none of the code you changed. Give
value-producing commands a plausible output: `--stub 'du=4096\t/some/dir'`.
Measured on a fixture: the empty stub printed `size=[]` / `branch skipped`
and never reached the guarded `sudo rm -rf`; the valued stub printed
`size=[4096]` / `BRANCH TAKEN` and captured it. Ask of every trace: _did this
actually go down the branch I touched?_
Measured on a fixture: the empty stub never reached the guarded
`sudo rm -rf`; the valued stub printed `BRANCH TAKEN` and captured it. Ask of
every trace: _did this actually go down the branch I touched?_
- **Stubs always exit 0.** A script that branches on a real failure status
takes a path it would not take live. Hand-write a stub for those and say so
in the split's evidence file.
@ -92,6 +88,11 @@ Three things to watch:
state harmlessly and a blanket stub would change what they see. Grep the
target for network and build verbs, then pass them:
`--stub git,curl,makepkg`.
- **`pkill` is NOT stubbed and `--prefix` cannot contain it** — signals are not
filesystem writes. `install_leechblock.sh:217` pkills every Chromium-family
browser before seeding, so tracing it closes the user's browsers (measured:
two probe runs fired it, harmless only because none was open). Grep a target
for `pkill`/`killall`/`systemctl stop` and pass `--stub pkill` first.
### `--prefix` — scripts whose job is placing files
@ -110,10 +111,9 @@ meta/scripts/trace_shell_split.sh nvidia_troubleshoot.sh --prefix /tmp/pfx-nv \
```
**Every run needs its own empty prefix**, hence the `-before`/`-after` naming.
Reusing one leaves the first run's files in place, so the second run's manifest
reports writes it never made and a dropped write stops showing up entirely —
measured: the fixture pair below diffs _clean_ on a reused prefix. The harness
refuses a non-empty prefix rather than deleting a path you named.
Reusing one leaves the first run's files in place, so a dropped write stops
showing up entirely — measured: the fixture pair below diffs _clean_ on a
reused prefix. The harness refuses a non-empty prefix rather than deleting it.
`HOME` **and every `XDG_*` base** are redirected. Exporting the XDG vars
explicitly is not belt-and-braces: `install_leechblock.sh` reads
@ -125,20 +125,18 @@ its size and a content hash. **That section is the point**: without it a
dropped `cat >` shows up as nothing at all — same exit status, same stubbed
calls, same stdout — and the traces match while one lost a file. Measured on
the `dropped_write_{before,after}.sh` fixture pair: with the manifest the lost
`.timer` is a one-line diff; with the section stripped the two traces are
byte-identical, i.e. the pre-`--prefix` harness passed that broken split.
`.timer` is a one-line diff; stripped, the two traces are byte-identical.
Five traps, each of which cost a debugging round:
Six traps, each of which cost a debugging round:
- **`/usr/bin` cannot be bound at all.** `--bind-abs /usr/bin` overlays the
directory the sandbox needs to exec anything; the run dies with
`bwrap: execvp bash: No such file or directory`. So a script whose last act
replaces a binary there (`install_pacman_wrapper.sh:304`,
`ln -sf … /usr/bin/pacman`) traces with everything else bound and **stops at
that line with exit 1** on EPERM. That baseline is stable across runs, but
proves nothing at or after the failing line — keep those statements in the
entry script. The scanner does not list `/usr/bin`: it follows variables into
`cat >`/`cp` and misses a literal `ln -sf` target.
directory the sandbox needs to exec anything (`bwrap: execvp bash`). A script
whose last act replaces a binary there (`install_pacman_wrapper.sh:304`,
`ln -sf … /usr/bin/pacman`) therefore **stops at that line with exit 1** on
EPERM. Stable across runs, so still a usable baseline — but it proves nothing
at or after the failing line, so keep those statements in the entry script.
The scanner misses `/usr/bin` here: it follows variables into `cat >`/`cp`,
so a literal `ln -sf` (or `cp "$f" "$f.backup.$(date …)"`) escapes it.
- **Bind leaf directories, never `/etc`.** Binding `/etc` wholesale shadows
`/etc/passwd` and `/etc/os-release`. Measured: `$SUDO_USER` lookup collapsed
to `/home//pyroveil`, `/etc/profile` became "No such file or directory", and
@ -146,9 +144,10 @@ Five traps, each of which cost a debugging round:
All three look like a broken split. The scanner therefore never emits a bare
`/etc`. A `--bind-abs` naming a **file** (`/etc/profile`) is bound
file-over-file, seeded with a copy of the original — bwrap cannot mount a
directory over a file, and binding the file's parent would be the wholesale
`/etc` bind again. The copy matters because a script that greps a config
before appending to it would otherwise read an empty file and branch wrong.
directory over a file. The copy matters because a script that greps a config
before appending would otherwise read an empty file and branch wrong. Note a
file bind cannot cover new _siblings_: `cp "$f" "$f.backup.<stamp>"` still
EPERMs, which is what truncates `nvidia_troubleshoot.sh`'s trace at step 3.
- **`require_root` truncates a stubbed run to nothing.** `lib/common.sh` does
`exec sudo "$0" "$@"`; under the `sudo` stub that records one line and exits
0 — a three-line trace that diffs clean against _any_ split. `--bind-abs`
@ -160,15 +159,16 @@ Five traps, each of which cost a debugging round:
at write time. Hashes are taken over content with the prefix normalized to
`@PREFIX@`; without that, a `mktemp -d` prefix makes every run differ.
- **A timestamp the target embeds still varies.** `nvidia_troubleshoot.sh`
writes `$(date)` into its config, so its hash changes every run; compare the
file list and sizes. Don't freeze the clock — that would hide real changes.
writes `$(date)` into its config, so its hash changes every run. Don't freeze
the clock and don't fall back to comparing sizes — diff the written files'
_contents_ with the date pattern normalized away. That still catches a
dropped or altered line, which a size comparison does not.
Anything absolute that was _not_ bound fails read-only/EPERM rather than
succeeding, so Decision 6 holds by construction. `--prefix` **refuses to run**
when the target writes to absolute paths and no `--bind-abs` was given, listing
the paths it found; the scan follows variables
(`MODPROBE_DIR="/etc/modprobe.d"` → `cat >"$CONFIG_FILE"`), because a
literal-only scan found nothing and produced a silent empty manifest.
when the target writes to absolute paths and no `--bind-abs` was given; the
scan follows variables (`MODPROBE_DIR="/etc/modprobe.d"` → `cat >"$CONFIG_FILE"`),
because a literal-only scan produced a silent empty manifest.
Verified untouched after the runs above: `~/.config/systemd/user`,
`~/.local/bin`, `~/.local/share`, `/etc/modprobe.d` (identical listings), and