The split referenced $_NL_PAYLOAD_DIR from both install functions but
defined it nowhere, so `setup_night_lockdown.sh setup` would have died
with an unbound variable partway through installing the lockdown. My
earlier scratch verification set the variable by hand, which is exactly
what hid the bug: the emitted artifacts hashed correctly while the real
entry path could not have reached them.
It is now defined in each lib that uses it, eight lines above the use,
via `: "${_NL_PAYLOAD_DIR:=...}"` so an explicit override still wins. A
definition placed in the entry script was silently stripped by an
autoformat pass three times during this work, and a half-run lockdown
installer is not an acceptable failure mode.
Also drops two zero-byte payload files left behind by the .sh -> .sh.in
rename.
Re-verified without presetting anything: install_enter_script and
install_unlock_script emit mode-0755 files hashing 4baaf405... and
eafce28d..., byte-identical to the live /usr/local/bin copies. help and
an unknown command match their baselines; status differs only by the
05:00 countdown ticking 13h to 12h between runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
918 -> 138 lines, across five libs plus two data-file payloads.
The two generated scripts became data files rather than heredocs.
ENTER_EOF alone was 279 lines, so any lib containing it would have been
over the cap however the rest was arranged. Both heredocs were quoted,
so their bodies were already literal text with no build-time
interpolation, and copying them verbatim is exactly equivalent.
The payloads are named *.sh.in on purpose. The pre-commit hook globs
staged files by '\.(sh|bash|zsh)$', runs shfmt -w on them and re-stages
the result -- and shfmt does reformat these bodies. That would have
changed their bytes and destroyed the property this split is verified
by. The .in extension keeps them out of every .sh-globbing hook without
a suppression or an exclude regex.
cat + chmod 0755 became a single `install -m 0755`, closing the window
the original left between creating the file and making it executable.
The load-bearing check: running install_enter_script and
install_unlock_script against a scratch directory produces mode-0755
files whose sha256 sums are identical to the copies already deployed in
/usr/local/bin on this machine. The split provably emits the exact bytes
that are running today, and it touched no system file -- sha256sum -c on
both deployed artifacts passes unchanged.
help, status and an unknown command are byte-identical to their
pre-split baselines. A line-multiset check leaves exactly six lines
unaccounted for: the two cat/heredoc openers, the two terminators and
the two chmod calls that install -m now performs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
21 assertions for cco_state.sh, the lib that decides whether a launch is
permitted. Coverage measured: 62/66 lines = 93.94%. The only uncovered lines
are kill_app's pkill/pkill -9 pair, which must never run in a test on a
machine whose apps are live. cco_wrapper/cco_install/cco_report stay at 0%:
every function in them moves system binaries, writes /etc/pacman.d/hooks or
kills processes.
The assertion that matters most is the one in the negative direction: a
PREVIOUS hour's stamp must NOT keep blocking. If that decision inverted, a
legitimately-closed app could never be reopened, and the failure would look
like the tool working. Both directions are pinned, plus the empty-stamp case
and stale suspend-marker cleanup.
Also generalises run_all.sh from test_leechblock_*.sh to test_*.sh. This
directory now holds suites for more than one entry script, and a prefix glob
silently skips every suite added after it — including inside the CI discovery
step added in fbec4656, which runs this file.
The harness asserts its own fixture is complete rather than letting a typo in
a global surface as a confusing unbound-variable from inside the code under
test. That check is also what makes the fixture's globals legible to
shellcheck, which cannot see them being read across the source boundary — a
real check that happens to remove SC2034, not a suppression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
705 -> 182 lines, with state, the launch gate, install/uninstall and
reporting moved into cco_state.sh, cco_wrapper.sh, cco_install.sh and
cco_report.sh.
The entry script keeps its exact deployed path and stays a real file
there, not a symlink: the app wrappers already installed on disk
hard-code /usr/local/bin/block-compulsive-opening.sh, and the pacman
hook invokes it. _CCO_LIB_DIR probes for lib/ beside the script and
falls back to the script's own directory, so the repo tree and the flat
/usr/local/bin deploy layout both work; both branches were exercised.
install_all deploys the libs and uninstall_all removes them in this same
commit, so there is never a deployed copy that is split but missing its
libs -- the failure that had to be repaired for install_leechblock.
The libs deploy flat into /usr/local/bin rather than into
/usr/local/share/digital_wellbeing/lib, because install_leechblock_payload
does an rm -rf mirror of that directory and would delete them.
Verified beyond the usual gates: a line-multiset check reports zero
missing functional lines, and all three heredoc bodies -- the generated
app wrapper, the generated pacman hook and the help text -- are
byte-identical, which verify_shell_split.sh cannot check. status output
matches the still-monolithic deployed copy byte for byte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pacman-wrapper installer copied install_leechblock.sh to
/usr/local/share/digital_wellbeing/ along with three data files, but no
lib/. After the split in 1dceca66 the deployed entry script sources
every install phase from lib/ beside itself, so deploying it without
that directory leaves a copy that dies on its first `source`.
This is not hypothetical: the live deployed copy was found in exactly
that state at the start of this work -- the previous session's split
entry script, deployed and then left behind when the repo was reverted.
It failed on every invocation. It has been repaired and now carries the
libs.
The glob is leechblock_*.sh rather than *.sh because lib/ is shared with
unrelated music_* helpers that have no business in /usr/local. The copy
mirrors rather than merges, so a lib deleted upstream does not linger.
Verified against a fake root and then for real: the deployed copy runs,
its sha256 matches the repo, and inject_default_config resolves the
defaults JSON and the seeder from the deploy root rather than from lib/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
29 assertions across two files, run via lib/tests/run_all.sh.
Coverage measured, not waived: leechblock_fetch.sh 38.57% (27/70),
leechblock_browsers.sh 36.47% (31/85), config and firefox 0%. The uncovered
lines are exactly the bodies that must never run under test — curl/tar/unzip
downloads, pkill + sudo binary patching, npm install, and writes to
/etc/*/policies.json. Closing the gap needs those externals shimmed.
What the tests actually pin down:
- all three get_latest_tag fallbacks (releases API, tags API, Location
header), plus JSON-null-is-not-a-tag and the all-sources-exhausted
non-zero return. A silent failure there installs the wrong version.
- the 8+3 browser tables including every hyphenated key, and the
command -v gating that decides whether a binary is patched at all.
Also fixes a latent set -u abort: wire_up_browsers interpolates VERSION_DIR,
VERSION and TAG, which only resolve_version sets. Calling it on its own
aborted on an unbound VERSION_DIR; they are now read through locals with
defaults, leaving production output unchanged.
Harness note: PATH must be narrowed to the shim dir for "no browser
installed" to be testable on a machine that has five, but every external the
code under test calls has to be seeded there too — a missing mkdir aborted
the suite under set -e with no stderr, which under kcov looked like a
coverage-tool bug rather than a PATH bug.
The hyphenated array subscripts in the test are quoted for the same reason
they are in the lib: shfmt -w rewrites an unquoted [google-chrome-stable]
into a subtraction expression. Reproduced live in this file during the work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
485 -> 114 lines, with the six install phases moved into four sourced
libs: leechblock_fetch.sh, leechblock_config.sh, leechblock_browsers.sh
and leechblock_firefox.sh.
detect_browsers, replace_browser_in_place and wire_up_browsers stay in
one lib on purpose: detect_browsers writes the BROWSERS/FIREFOXES arrays
that wire_up_browsers reads, and splitting them would create a
write-only cross-file global.
Four justified body changes, all recorded in the evidence:
- Quoted the six hyphenated associative-array keys. shfmt 3.13.1 parses
an unquoted [google-chrome-stable] as arithmetic and rewrites it to
[google - chrome - stable], which would break browser detection
entirely. The pre-commit hook runs shfmt -w on staged files and
re-stages them, so this corruption would have landed in the commit
unseen -- this is what caused the previous attempt at this split to be
reverted. Behaviour-preserving: quoted and unquoted are the same key.
- SCRIPT_DIR is assigned in the entry script rather than inside
inject_default_config. Left in the lib, ${BASH_SOURCE[0]} resolves to
lib/, so the defaults JSON lookup would miss and default blocking
rules would silently never be seeded.
- install_firefox_policy takes ff_found as a parameter instead of
reading it as a cross-file global, fixing a genuine SC2154 without a
suppression.
- Added require_cmd unzip. It was used bare under set -Eeuo pipefail, so
a missing unzip would abort after the version dir was populated but
before the 'current' symlink was made, leaving a half-installed tree.
verify_shell_split.sh reports additions only -- no -/+ pair -- so
get_latest_tag and replace_browser_in_place moved byte-identically.
--help and unrecognized-flag output are byte-identical to the baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
913 lines down to 150. The ten functions were only 202 lines; the other 712
were a single top-level block, now eight phase functions across six libs. As
in the previous split the bodies were wrapped WITHOUT re-indenting, because
five heredocs use quoted terminators that must stay at column 0 (shfmt
indented them afterwards, which is safe).
The ~320-line custom-blocklist heredoc became custom_entries.hosts. It was
321 of the 370 lines of hosts_write.sh and cannot be split, so that lib could
not otherwise fit the cap; a hosts-format data file is also far easier to
audit and diff than a quoted heredoc.
That move broke a guard, which is the important part of this commit:
extract_custom_entries_from_script sliced the blocklist out of the script
between "# Custom blocking entries" and the heredoc terminator. With the
entries in a data file there is no terminator, so it would have extracted
NOTHING -- and check_custom_entries_protection, the guard whose entire job
is refusing to drop blocked domains, would have compared against an empty
list and passed everything. It now reads the data file, and the extracted
set is identical: 254 domains, diffed.
A second, pre-existing bug surfaced while testing:
extract_unblock_entries_from_script matched its OWN marker lines.
PROTECTED_UNBLOCK_LIST_START and _END are word characters, so the
'^# [a-zA-Z0-9._-]+$' pattern pulled both into the whitelist as if they
were domains. Both are in this machine's live state file right now.
Renaming either marker would have read as the whitelist growing and blocked
the install outright.
There is no dry-run or status mode, so this file cannot be proven by running
it: an aborted run leaves /etc/hosts mutable with the guard's bind mount down.
The proof is textual instead. Every one of the 8 wrapped phase bodies is
byte-identical to its original line range; a line-multiset check found 0 lines
lost; and an indentation-insensitive comparison against the pre-split file
leaves exactly 8 absent lines, each an intentional edit (the rewritten
extractor, three `readlink -f "$0"` made overridable, the /etc/hosts fallback
made overridable, and the heredoc markers). /etc/hosts is byte-unchanged
across the whole session, and `--help` output is identical.
36 assertions. Both anti-tamper guards at 100% line coverage (75/75 and
49/49), covering both directions of each: removing a blocked domain is
refused and adding one allowed; growing the whitelist is refused and shrinking
it allowed. A same-count swap is refused too, which a count-only check would
have let through.
The install phases themselves are deliberately untested -- running them would
take down the protection they install. That is weaker evidence than the two
previous splits had, and it is inherent to this file rather than a gap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
1017 lines down to 148. Six libs: the per-language doc-URL builders (split in
two only for the cap), the lookup/dispatch layer, and one per generation phase.
The hard part was not the ten functions but the 335-line top-level block,
which extract_shell_functions.py cannot move because it only relocates
functions. It is now generate_doc_links / generate_anki_cards /
generate_llm_prompt, wrapped WITHOUT re-indenting the bodies: five heredocs
use quoted terminators, which must stay at column 0.
Proof the split is behaviour-preserving: a golden fixture captured before any
edit, re-checked after every step. All three outputs -- anki_cards.txt,
documentation_links.md, llm_anki_prompt.md -- stay byte-identical by sha256sum,
and stdout matches too.
Four latent defects surfaced while writing the tests, all fixed:
python_doc_url required a $2 it never reads, and get_doc_url has always
called it with one argument. That only worked because the entry script runs
`set -e` without `-u`, so "$2" silently expanded to empty; any caller with
`set -u` aborted. Defaulted, along with three more positionals documented as
optional but declared required.
The two imports loops hardcoded `head -20` while the other seven honoured
TOP_N, so --top was silently ignored for imports.
Only one of nine term loops skipped comment lines, so a '#' line in an
analysis file became a flashcard whose front was a comment.
Introduced and fixed within this change: the comment filter has to run
BEFORE `head -n` or comment lines count against the limit, and it needs
`|| true`, since grep exits 1 when it filters every line and the callers run
under `set -e`.
The golden fixture cannot show the last three, because it has no comments and
fewer than 20 imports -- which is exactly why they survived. A second fixture
with 25 imports and comment lines, at --top 3, emitted 20 imports and two
comment cards before, and 3 imports with zero leakage after.
166 assertions across 4 test files. 100% line coverage on study_doc_urls.sh
(38/38), study_doc_urls_more.sh (30/30), study_doc_lookup.sh (49/49) and
study_gen_llm.sh (54/54). study_gen_docs.sh 93.10% and study_gen_anki.sh
96.15%, where every remaining line is a `} >>"$FILE"` closing a brace group
whose output a passing test asserts -- the same kcov attribution artifact
recorded for services_hosts.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
Eight checks assigned status="warning" unconditionally, so a check running
later in the same function overwrote the status="error" an earlier one had
set. The five other sites in this family already guard the assignment with
`if [[ $status != "error" ]]`, so this was an inconsistency, not a design.
Only half of it was load-bearing, and the two halves fail differently:
services_units.sh (4 sites) reports through report_and_fix, which repairs
ONLY on "error". A downgrade there meant no repair ran at all. The is-active
check is the LAST status assignment in each of the three functions, so it
could mask any error above it -- in check_periodic_systems, three separate
is-enabled checks. A timer that was both disabled and inactive, which is the
ordinary shape of a broken timer, was reported and then left broken.
services_hosts.sh (4 sites) gates its own repair on `status != "ok"`, so the
repair still ran. There the bug printed a real error as a yellow [!] warning
and understated the summary row.
Demonstrated rather than argued, against a simulated broken machine (timer
disabled AND inactive, unit files present):
without the guards: status=warning, repair ran 0 times
with the guards: status=error, repair ran 1 time
The seven assertions that previously pinned the broken behaviour now assert
the correct behaviour; they were rewritten first and failed, then the guards
made them pass. Two further cases cover the masking shape directly: a broken
guard-lib instance must still be repaired when a later check warns.
`--status --dry-run` output is still byte-identical to rev 6691cb26, because
this machine has no disabled timer and no missing pacman hook -- the buggy
path is never taken here, which is precisely why it went unnoticed. The A/B
above is the evidence the live diff cannot provide.
222 assertions across 10 files. services_units.sh holds 100% coverage;
services_hosts.sh rises from 86.05% to 93.02%. test_services_hosts.sh needed
splitting twice to stay under the 250-line cap (hosts_fix, resolved), and
stage_hosts_ok moved into the harness so both new files can use it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
Follow-up to 9085a4e5, which recorded services_units.sh at 84.51% because
its remaining branches sit behind [[ -f /etc/systemd/system/*.service ]] --
absolute root-owned paths that exist on this machine, so the else-branch
never runs and no PATH shim can redirect it.
All 87 such paths across the seven check libs now go through $SYSROOT, read
from SERVICES_ROOT. The prefix is deliberately NOT defaulted. Three commands
in hosts_fix_resolved run outside `run` and so are not stopped by --dry-run
(chattr -i on resolved.conf.d, find -delete over its *.conf, and an append to
resolved.conf); with an empty default, a test that forgot to set SERVICES_ROOT
would really delete this machine's resolved.conf drop-ins. Unset is therefore
a hard error and only an explicitly empty value means the real filesystem.
The harness now owns PATH outright: $FAKE_BIN for shims and staged fake apps,
$REAL_BIN for a curated set of symlinks to genuine coreutils. The machine's
/usr/bin is deliberately absent -- a really-installed discord and chromium
otherwise make the "no target apps present" branches untestable, which is
exactly what happened until these tests caught it.
Coverage, measured against run_all.sh because two test files jointly cover
services_common.sh after the 250-cap split: common 55/55, report 27/27,
units 72/72 (was 84.51%), wrappers 100/100, apps 66/66, browser 79/79 --
seven libs at 100%. services_hosts.sh reads 86.05% and services_hosts_fix.sh
97.30%, and those 14 lines are a kcov attribution artifact rather than a gap.
Falsified rather than assumed: the suite prints "51 passed, 0 failed" when run
under kcov, so no group is skipped, and every supposedly-uncovered line is an
issues+=("...") whose string can only reach stdout through the print loop
below it -- with a passing assertion matching that exact string.
Two falsification checks, both re-run after the rewrite: `--status --dry-run`
as root stays byte-identical to rev 6691cb26 modulo the Date: line, and the
sha256sums of /etc/hosts, /etc/nsswitch.conf and /etc/systemd/resolved.conf
plus the contents of resolved.conf.d are unchanged across a full suite run.
The second one is what makes a missed prefix site detectable instead of a
matter of inspection: the repair paths really do write.
220 assertions across 8 test files. Also fixes a bug in the harness's own
lsattr shim, where printf with a leading-dash format string parsed the dashes
as options and silently produced nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
1225 lines down to 202, with the 21 functions moved into eight sourced
libraries grouped by subject. The entry script now holds only the
configuration, the argument parsing and the order the checks run in.
check_hosts was the one function that could not move verbatim: at 260 lines
it would have stayed over the cap on its own, so its repair half is now
hosts_fix_nsswitch / hosts_fix_resolved / hosts_repair_all in
services_hosts_fix.sh. Repair order is preserved (resolver config, then the
hosts file, then the guard-lib migration), and hosts_repair_all returns
non-zero under --dry-run so a dry run can no longer promote a status to "ok"
on the strength of repairs that never happened.
SERVICE_STATUS was a write-only global across five libs, which is a real
SC2034 rather than noise: the pre-commit hook runs shellcheck with no -x and
genuinely cannot see the one reader. It now goes through set_service_status /
get_service_status, which also rejects a duplicate write — each check owns
exactly one summary row, so a second write means two checks are fighting over
it. No suppression was added anywhere.
Proof the behaviour is unchanged: `--status --dry-run` run as root against
this machine is byte-identical to the same command from a worktree at the
pre-split revision, modulo the Date: line. verify_shell_split.sh reports
check_hosts as the single expected difference; the other 20 functions hash
identically. 72 assertions across three test files pass, with 100% line
coverage on services_common.sh (54/54) and services_report.sh (27/27).
services_units.sh measures 84.51% (60/71). The 11 uncovered lines are the
else-branches of [[ -f /etc/systemd/system/*.service ]] and
[[ -f /usr/local/bin/*.sh ]] probes — absolute root-owned paths that exist on
this machine and that no PATH shim can redirect. Reaching them needs a
root-prefix rewrite of live enforcement logic, which is not a line-count fix
and would forfeit the verbatim-move proof for everything else.
Found while testing, deliberately NOT fixed here: in all three unit checks
the is-active branch assigns status="warning" unconditionally, overwriting
the status="error" the is-enabled branch set just above it. A timer that is
both disabled and inactive therefore never gets repaired, because
report_and_fix only repairs on "error". It is identical in the pre-split
file, so this split neither caused nor hides it. The tests pin the current
behaviour and name it; fixing it changes enforcement and earns its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
Regression from this session's earlier commit (327d48b): the test still
referenced pacman-hooks/*.sh and install_pacman_hooks.sh, which that commit
archived to testsAndMisc-archive. shell-tests.yml runs this test directly
(not scoped by pre-commit's --files), so it went red on CI without being
caught locally before the push.
Dropped the assertions tied to the archived files; kept and extended the
ones checking pacman_wrapper.sh's guard-lib fallback logic, which was
already fully guard-lib-native and unaffected by the archive.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
setup_hosts_guard.sh was NEXT_SESSION_PROMPT.md's next split target, but
live-machine inspection showed it (and 16 supporting files - enforce-*.sh,
6 systemd units, install_pacman_hooks.sh, pacman-hooks/, psychological/,
both READMEs) is fully retired in favor of guard-lib, migrated 2026-07-04.
User confirmed archive over split, then confirmed the larger 17-file scope
once the directory was surveyed. Full history preserved via git filter-repo,
live at github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/periodic_background/hosts/guard.
plugins/{nsswitch,resolved}-plugin.sh stay - they're guard-lib's live
plugins, referenced by absolute path from /etc/guard-lib/targets/*.conf.
check_and_enable_services.sh's check_hosts() rewritten to check guard-lib
state (guardctl file-guard status) instead of the retired units/scripts it
used to check and self-repair - verified live, now reports the hosts
service ok against the actual migrated machine instead of a false error.
copilot-instructions.md and one install.sh comment updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Confirmed live (not dead code) via the dead-code check-in before any code
was touched -- the last two candidates this pattern was tried on were both
archived instead. Kept run_one/cleanup/log_size/main together in the entry
script: a prior session (2527fb3) already proved splitting run_one out
produces a real SC2153/SC2034 pair since PACMAN_PID/LAST_ELAPSED cross the
EXIT trap, not a suppressible false positive.
Fixed a real bug found while writing tests: print_summary's inline awk had
a dead `NR == 0` guard (printf on an empty array still emits one blank
line), reachable via `-n 0`. Refactored mem_available_mb, descendant_pids/
kill_tree, capture_stall's two output blocks, and print_summary's median
calc after confirming empirically (bash -x tracing) that kcov's
xtrace-based coverage can never see a `done <redirect>`, `} >redirect`, or
inline-awk line -- user chose to fix the code over the coverage tool.
descendant_pids specifically needed an explicit-fd process substitution
(not a pipe) to keep its array mutation in the current shell.
Made PACMAN_BIN/PACMAN_LOG/CACHE_DIR/PACMAN_LOCK/LOAD_FLOOR_MB/
LOAD_MIN_FREE_MB env-overridable for testing. Caught and fixed, before it
ran unbounded, a --with-load test that would have dd'd 13-20+ GB into
/dev/shm using this box's real available memory.
56 lib-level + 13 entry-level PATH-shim/subprocess assertions, 0 failed.
The entry test itself grew past 250 lines while adding coverage and had to
be split into a shared harness plus two test files, mid-session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Full history preserved via git filter-repo, including both pre-reorg
root paths (libre_translate.sh and the subtree-imported
Bash/libre_translate.sh) since both were genuine ancestors of the
current file. Live at
github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/single_use/misc/testsAndMisc-bash.
Asked the dead-code check-in before any split work started this time,
per the lesson from the plagiarism-tools session. NEXT_SESSION_PROMPT.md
now points at diagnose_pacman_hook_stall.sh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Full history preserved via git filter-repo, including the pre-rename path
from before the single_use/periodic_background split. Live at
github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/single_use/utils.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Wraps the four install stages into named functions, one per lib/plagiarism_*.sh
sibling (matching install_exercism.sh's convention), and extracts the two
embedded Python heredocs into real, lint-clean check_plagiarism.py/sherlock.py
files -- check_plagiarism.py needed a second split into plagiarism_similarity.py
to also clear the 250-line cap. Every function reaches 100% shell coverage via
a PATH-shim test harness (fake python3/pip/git/java/wine); the two Python CLIs
were separately verified against a real venv with actual nltk/scikit-learn.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Grouped by the state each function touches rather than by size:
steam_requirements.sh 199 requirement parsing, scoring, and the hardware
detection that feeds it
steam_report.sh 156 the run loop and the cache it reads
steam_api.sh 124 library discovery, Steam and ProtonDB calls
steam_credentials.sh 103 credentials, usage text, presentation helpers
The grouping was forced by the SC2034 writer/reader rule, not chosen. Two
arrangements were discarded: detect_system alone in a lib writes the three
SYSTEM_* globals without reading them, and load_cache_map alone writes
CACHE_MAP without reading it. Both were fixed by putting the writer next to
its reader - detect_system beside score_game, load_cache_map beside main.
Verified: all 33 functions verbatim; every lib passes shellcheck standalone
with no suppression; and the script actually runs - `--help` prints the usage
block and exits 0, which is what proves the four source lines resolve and
main still dispatches.
Over-cap files 26 -> 25.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
clean_audio.sh 419 -> 230 + 204
enforce_vbox_hosts.sh 443 -> 220 + 236
migrate_hosts_guard.sh 444 -> 167 + 162 + 141
migrate_hosts_guard needed two libraries: with all ten work functions moved
the single lib was 295 lines, so migrate and rollback were separated.
The extractor's brace matcher required index > start and so silently skipped
one-line `warn() { ...; }` definitions. Fixed, and regression-checked against
the committed fresh-install/main.sh split byte-for-byte.
The harness earned its keep on a fourth file: adding `export` to libre_translate
to satisfy SC2034 changed two function bodies, and verify_shell_split.sh caught
what lint had accepted. Those exports were reverted, and libre_translate.sh is
parked - what remains after its clean seams both assigns and reads the same
globals, so getting it under the cap needs parse_args restructured rather than
relocated, which is a refactor rather than a verbatim move.
That cost is now written down: a lib must contain the readers of every global
it assigns. The precedent at install_pacman_wrapper.sh:29 is about
definitions-only libs, not about assignments as such, and the pre-commit hook
runs shellcheck with no -x, so a source= directive will not make it follow the
link. This is the shell analogue of the monkeypatch-target trap.
Verified: all 8, 13 and 19 functions verbatim; all four libraries source with
no side effects; shellcheck passes standalone on every file with no
suppression; bash -n passes on all seven. None of the three was executed -
they drive ffmpeg over real media, mutate VirtualBox guest disks, and rewrite
systemd units.
Over-cap files 29 -> 26.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Two single-use scripts with no deployed copy, so the entry+lib shape is
safe. Each keeps the ordering, the safety checks and the user-facing output;
the configuration writers move to a library beside it.
Both scripts write `set -e # Exit on any error`, so extract_shell_functions.py
now matches the directive rather than the whole line. Regression-checked by
re-deriving the committed fresh-install/main.sh split byte-for-byte.
Verified: all 6 and all 9 top-level functions verbatim via the shfmt
harness; both libraries source with no side effects and define every
function moved into them; bash -n passes on all four.
Neither script was run. nvidia_troubleshoot.sh writes /etc/X11 config and
installs pyroveil; setup_passwordless_system.sh writes sudoers and lightdm
autologin, which is what this machine's login depends on.
Over-cap files 31 -> 29.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The five package-installation functions move to lib/packages.sh; main.sh
keeps the top-level flow and sources the library after `set -e`.
The first attempt sliced by line range and was thrown away. This file
interleaves top-level commands - sudo -v, git config, the cp of
makepkg.conf and pacman.conf - between its function definitions, so a range
slice swept them into the library, where they would have run at source time
and in the wrong order. Sourcing that version blocked on a sudo password
prompt, which is how it was caught. The rebuild extracts function blocks by
brace counting and leaves every other line exactly where it was.
Verified with the new shfmt harness (all 5 functions verbatim), by a
structural comparison of the top-level flow showing exactly one line added
and none missing, and by sourcing the library to confirm it defines all five
functions with no side effects. The script itself was NOT run: it does
`sudo pacman -Sy`, overwrites /etc/makepkg.conf and /etc/pacman.conf, and
installs AUR packages.
Over-cap files 32 -> 31.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The last Python file over the cap:
_transcribe_media.py 143 _try_import, both duration probes,
get_media_duration, ffmpeg transcode, cleanup
_transcribe_cluster.py 105 _resample_linear, _kmeans_cosine and its
iteration loop
_transcribe_diarize.py 164 the pipeline: audio loading, the classifier,
embedding extraction, diarize_segments
The graph is a tree: _transcribe_media imports nothing local, _cluster takes
only _try_import from it, _diarize imports from both. All 13 top-level
definitions verified AST-identical to HEAD, and an AST scan confirms the
three module constants travelled to the modules that use them.
transcribe_fw.py now imports get_media_duration from _transcribe_media
rather than through _transcribe_diarize. Ruff removed that re-export as
unused and broke the import; running the program is what caught it, and
naming the owning module is the correct fix under the no-barrels rule.
Verified: transcribe_fw.py /nonexistent.wav still exits 2 and --help exits
0; 63 tests pass at 100% line and branch on all three modules; 338 pass in
the directory. Over-cap files 33 -> 32, and no Python file is over the cap
any more - the remaining 32 are 29 shell, 2 kotlin, 1 dart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The largest untested Python file left in the repo, at 356 lines against the
cap. Tests land first so the split that follows is verified against tests
that predate the seam.
63 tests: duration probing through both ffmpeg paths, the transcode fallback
and temp-file cleanup, audio loading's three outcomes, linear resampling,
cosine k-means, the ECAPA classifier loader, per-segment embedding
extraction, and diarize_segments end to end.
numpy is real here, not a double. It is a declared dependency, and the core
of this file is clustering maths - assertions against a mocked numpy would
prove nothing about whether two speakers actually get two labels. The
_try_import seam passes numpy through while stubbing torch and speechbrain,
so the tests still run in CI with neither installed. The clustering tests
assert real properties: two opposed groups separate, magnitude is ignored
because the metric is cosine, and a fixed seed reproduces.
The last three coverage gaps were _load_audio's ffmpeg fallback in both
outcomes, the minimum-slice widening, and the zero-iteration k-means branch.
Split across six test modules after two intermediate arrangements went over
the cap, and one move silently broke a test by separating it from its
doubles - caught by the test count holding at 63 through every rearrangement.
338 tests pass in the directory, up from 275 by exactly the 63 added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
_transcribe_progress.py 91 _run_progress_loop, _format_progress_line,
and the two constants only they use
transcribe_fw.py 236 argument parsing, model loading, diarized
output writing, main
All 8 top-level definitions verified AST-identical to HEAD, and an AST scan
confirms neither module references a name it does not bind - which is what
caught that the two progress constants and the hhmmss import had to travel
with the loop. transcribe_fw.py no longer imports time.
The progress tests now patch _transcribe_progress, including its sys.stderr
and time references, since that is where the loop resolves them.
_resolve_device_and_compute stayed behind, so its test still points at
transcribe_fw.
Verified: --help prints the full usage and a missing input returns exit 2,
the code transcribe.sh branches on; 32 tests pass at 100% line and branch on
both modules; 275 pass in the directory. Over-cap files 34 -> 33.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
transcribe_fw.py is 302 lines against the cap and was untested. Tests land
first, in their own commit, so the split that follows is verified against
tests that predate the seam.
32 tests: argument parsing and its FW_* environment defaults, device and
compute resolution, the progress line, the transcription loop, model
loading, diarized output writing, and main()'s three exit paths.
faster_whisper is reached only through _try_import, so patching that seam
covers every path without the package installed. _format_progress_line is
tested directly rather than through the loop - it is pure, so the percentage
clamp, the ETA, and the suppression of an ETA longer than a day are each
assertable without a model.
The last coverage gap was _try_import itself, which every other test
patches; it is now exercised directly in both directions.
Split across three test files because a single pipeline module reached 321
lines - over the cap this work exists to clear. 275 tests pass in the
directory, up from 243 by exactly the 32 added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Split along the natural seam now that the file is fully covered:
_transcribe_probes.py 177 _try_import, the four dependency probes,
generate_sine_wav, prepare_model, test_cuda
transcribe_helpers.py 142 the eight _handle_* functions and main()
All 18 top-level definitions verified AST-identical to HEAD, and an AST
scan confirms neither module references a name it does not bind.
transcribe.sh is untouched - it invokes tools/transcribe_helpers.py by
path, and that path still exists with the same name and executable bit.
The CLI tests' patch targets did not move either, because main() dispatches
to names imported into its own namespace.
Verified by running every subcommand and reading the exit status directly,
since those codes are the contract transcribe.sh branches on:
python-version 0, check-faster-whisper 7, check-ctranslate2 1,
check-diarization 0, deps-installed 0; generate-wav writes the same
96044-byte WAV as before the split. 41 tests pass at 100% line and branch
on both modules; 243 pass in the directory. Over-cap files 35 -> 34.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
transcribe_helpers.py is 289 lines against the 250-line cap and was
completely untested. Tests land first, in their own commit, so the split
that follows is verified against tests that predate the seam.
Left at its current path rather than moved under python_pkg/: transcribe.sh
invokes it by path inside a dedicated ML venv, and transcribe_fw.py imports
two siblings by bare name, so the closure is five files (1236 lines), not
three. Tests go in linux_configuration/tests/ instead, whose conftest
already puts standalone script directories on sys.path - the same
arrangement the usage_report work used, and still collected by CI.
The ML dependencies are never installed in CI and the module reaches them
only through _try_import, so patching that one seam covers every probe in
both directions. An AST scan at every nesting level confirmed there is no
other path to them: the only direct numpy imports sit under TYPE_CHECKING.
41 tests, 100% line and branch. The last gap was the dispatch guard at
line 284, unreachable through the CLI because argparse's choices list and
the dispatch dict carry the same eight commands; covering it by admitting a
ninth choice also documents why the guard is there.
Split across four test files because a single module hit 291 lines - over
the cap this work exists to clear.
Verified: python-version prints 3.14, check-faster-whisper exits 7 with no
ML deps present, generate-wav writes a 96044-byte WAV; 243 tests pass in the
directory, up from 202 by exactly the 41 added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Documented as "kept for backwards import compatibility", it had zero
production callers - only its own two tests. Removed the function, those
two tests, and the pathlib.Path import that only its signature used.
_SEC_PER_DAY stays: two live callers remain at _usage_report_run.py:135
and :181, so only the function was dead, not the constant.
Verified: usage_report.py --help exits 0 (the unit tests stub subprocesses,
so only a real run proves the import graph), 202 tests pass, and
_usage_report_run.py holds 100% line+branch coverage after removing both
the code and the tests that covered it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The entry point now holds only the parser, mode dispatch and main. Three
modules take one concern each: _usage_report_logs (where the logs are,
the state file, per-day segment planning), _usage_report_emit (preflight,
stdout, clipboard), _usage_report_run (the two run paths and the
aggregation they drive).
Verified by the 100%-coverage tests from the previous commit. An AST
comparison of all 22 top-level defs and classes reports 22 identical, 0
mismatches.
All four modules hold 100% line+branch. 204 tests before and after. Both
CLI modes were run against the machine's real logs, which is the check
the unit tests cannot make: they stub the collaborators, so only a real
run proves the new import graph resolves.
Two lessons, recorded because the next target is a 498-line file:
This cut was too big. Two names had to be relocated after their first
placement created an import cycle, and four modules' worth of monkeypatch
targets needed re-pointing. A smaller first slice would have been cheaper.
Patching a moved function must target the module whose namespace resolves
it - the caller's, not the one it was moved to. Getting that wrong on
_run_since made main() call the real function, which read the actual atop
logs and hung the suite rather than failing.
Three import bugs (Path and _fmt_h left under TYPE_CHECKING but used at
runtime, dataclass missing where _Aggregates landed) were caught
immediately by the coverage from the previous commit.
Over-cap 38 -> 37.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
25 tests in four files, no production change - the module is
byte-identical to HEAD. Lands before the split for the usual reason.
Covers the CLI surface (every default, every flag, main's dispatch), both
run paths including the shared empty-window-means-a-full-day fallback,
_run_since's no-data path and both state-advance branches, the
clipboard/emit side (tool selection, a failing tool, no tool at all),
_preflight's three outcomes, _aggregate_segments with a gap day and with
a present-but-empty log, and _log_descriptions.
Also covers _compute_window, which turns out to be dead: a shim
documented as "kept for backwards import compatibility" with zero callers
anywhere in the repo. Tested rather than deleted here, since removing it
is a behaviour change that does not belong in a coverage commit - but it
is a deletion candidate.
185 stmts / 44 branches, 0 missed. Directory: 179 -> 204 passing.
No suppressions: mypy rejected a `list.append(...) or 0` lambda, replaced
with a real closure; ten ruff S108 findings on hardcoded /tmp paths became
/nonexistent sentinels, which is more accurate anyway since those paths
are never opened.
Written as two files of 273 and 282 lines, then split to four under the
cap before committing - the DropZone lesson from earlier today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Lifts the atop subprocess machinery into _usage_report_atop.py: running
atop, streaming its parseable output, building/caching the atop_agg C
helper, and reading its TSV. What stays is record parsing, epoch windows
and per-day merging.
Verified by the 100%-coverage tests from the previous commit. An AST
comparison of all 16 functions against HEAD reports 16 identical, 0
mismatches.
_usage_report_render.py imported _run from the parsing module; it now
imports from the module that owns it. That is the same re-export barrel
the last split shipped and had to fix in a follow-up - caught in the same
commit this time. usage_report.py needed no edit at all.
Both modules hold 100% line+branch (94/28 and 130/34, 0 missed). 179
tests before and after; the five new test files collect 8+6+4+14+5 = 37,
which is last commit's 36 plus the one re-homed out of
test_usage_report_modes.py, where it referenced a constant that had
moved out from under it.
The two test files added last commit were themselves over cap (373, 272)
so they are split five ways here - the DropZone lesson from earlier
today, where a lint fix silently added an over-cap file.
Ran the program: a real report from today's logs, through the native
atop_agg path, which is precisely the code this commit moved.
Over-cap 39 -> 38.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
36 tests, no production change - the module is byte-identical to HEAD.
Lands separately from the split that follows, because tests written in
the same commit as a split are written against the post-split shape.
This one mattered more than the last: the uncovered 61% was almost
exactly the code a split would extract - the subprocess plumbing and the
native atop_agg path. Splitting that first would have proved nothing.
_atop_agg_binary had no tests at all; all five outcomes are now covered
(fresh cache, no compiler, make raising, make exiting 0 with no binary,
successful build + chmod). The fresh-cache test asserts by installing a
subprocess.run that fails if called. Every subprocess is faked - no test
spawns atop, make or cc.
Also covers _run's two error paths, _iter_atop_lines with and without a
stdout pipe, the native N/W TSV rows including malformed ones,
_parse_name's six paths, _parse_prm, _window_from_epochs' median
interval, _fold_pid_aggregates, and aggregate_atop end to end.
219 stmts / 62 branches, 0 missed. Directory: 143 -> 179 passing. No
suppressions - two mypy exit-return findings were fixed by typing the
fakes' __exit__ as None rather than silencing them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The previous commit's message claimed "no re-export barrel". That was
wrong: usage_report.py still did
from _usage_report_render import _fmt_h, _render_report
and after the split _fmt_h lives in _usage_report_format - the render
module only re-imports it for _methodology_section. So the import reached
the owner through an intermediary, which is the exact pattern the brief
says was vetoed last session. Now each name comes from its own module.
Also untracks bucket_catch/packages/frontend/tsconfig.tsbuildinfo and
gitignores *.tsbuildinfo. It is a generated incremental-build cache
tracked in a repo that blocks binaries, and it has been dirty in every
commit this session, forcing pre-commit to stash each time.
143 tests pass; usage_report.py still generates a real report.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Verified by the 100%-coverage tests from the previous commit, written
before this seam existed.
_cpu_table/_dedupe_ram/_ram_table/_gpu_table move to
_usage_report_tables.py with the two constants they own. _md_escape and
_fmt_h move to _usage_report_format.py: both the tables module and the
render module need them, so a third module is what avoids a cycle.
Import sites name the owning module; no re-export barrel.
usage_report.py needed no edit - it imports _fmt_h and _render_report
from the render module, and both names still resolve there.
Evidence the move was verbatim: an AST comparison of all 10 functions
against HEAD reports 10 identical, 0 mismatches. That matters because I
retyped rather than cut-and-pasted, and did briefly introduce a literal
× where the original had × - ruff's RUF001 caught it.
Coverage stays 100% line+branch on all three (15/55/54 stmts, 4/10/14
branches, 0 missed). Test count 43 before and after, summed across the
five files it now lives in - the check that matters for a test split,
since a file outside the runner's glob is silently never collected.
Directory total 143 passed, unchanged.
Ran the real program: a full report from today's atop + pmon logs, every
section rendering through the new boundaries including the RAM sibling
dedupe and its (+20 more) truncation.
Over-cap 41 -> 39. No suppressions; a shared test-helper module was tried
and abandoned because name-tests-test would have needed an exclusion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
43 tests, no production change - the module is byte-identical to HEAD.
This lands separately from the split that follows because a split
verified by tests written in the same commit proves nothing: the tests
would have been written against the post-split shape.
Covers every function, and specifically the branches a happy-path test
misses: zero-length window and cpu_count() -> None in _cpu_table,
non-positive RSS and the CPU/pid tie-break in _dedupe_ram, the sibling
truncation boundary in _ram_table, zero total_samples in _gpu_table,
OSError and unparseable MemTotal in _host_profile, and the single-sample
unknown-interval path in _methodology_section and _render_report.
_host_profile is the only impure function; its /proc reads go through a
monkeypatched Path.open and nvidia-smi is stubbed at the _run seam, so
no test touches the disk or forks.
117 stmts / 28 branches, 0 missed. linux_configuration/tests: 100 -> 143
passing. No suppressions: two ANN401s were removed by typing the helper
kwargs explicitly rather than silencing them, and a mypy ignore went away
with a fallback branch no caller reached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Three units along the file's own concerns:
atop_agg.c (204) ingest -- tokenising atop output, folding lines, main
atop_agg_state.c (113) the per-PID hash tables, slot lookup, epochs, alloc
atop_agg_emit.c (178) the ranked, name-aggregated report
Table geometry and the four table functions that now cross a unit boundary go
in a private atop_agg_internal.h. Those four lose `static` -- the one
visibility change here.
The Makefile edit is the load-bearing part: SRC had to grow or the build would
have linked the old single unit and the suite would have passed with the new
files unused. Checked by reading the compile line, not the exit status.
I had written this file off as unverifiable earlier. Wrong: the Makefile has
had test and coverage targets all along. Coverage is 92.9% lines / 100%
functions on the same 211 source lines, before and after -- the best verifier
of anything left over the cap. Production binary builds at -O2 and runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Split three ways on the file's own section banners, which already grouped the
tests by concern:
test_usage_report_merge.py (145) aggregate and window merging, PID carry-over
test_usage_report_since.py (186) pmon filtering, atop bounds, state, planning
test_usage_report_modes.py (178) start resolution, dispatch, PRC HZ, native
6 + 13 + 11 = 30, the pre-split count -- the check that matters, since a file
that failed to import would collect nothing and still report success. Also
verified a negated assertion still fails, so none of the three is vacuous.
Only the test file is split. The usage_report SOURCE files stay over cap on
purpose: measured coverage is 21% for _usage_report_render.py, 39% for
_usage_report_parsing.py and 47% for usage_report.py, so a split there would
move code no test runs and pass either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
setup_thorium_startup.sh (443 lines) was queued for a split under the 250-line
cap. It turned out the feature it installs has been broken since Thorium was
removed in favour of chromium/librewolf.
/usr/local/bin/thorium-browser is now only a leftover symlink to
browser-preexec-wrapper, which routes by basename to /usr/bin/<name>. With no
/usr/bin/thorium-browser it falls into its testing branch and treats the URL as
the binary, so every boot logged:
thorium-browser: line 33: /https://www.fitatu.com/app/planner: No such file
while systemd reported status=0/SUCCESS and "Finished". The success status is
why this went unnoticed.
Splitting a 443-line script to keep a dead feature under a line cap is the
wrong work, so it is deleted instead, along with its checker.
Removing check_thorium_startup() from check_and_enable_services.sh is the part
that makes this stick: its report_and_fix RE-RAN the installer whenever the
service was not enabled, so deleting only the deployed units would have let
them come back. Verified afterwards that the hourly maintenance timer runs a
deployed script with no thorium reference, and that nothing on a timer invokes
the checker at all.
Also removed from fresh-install/main.sh so new machines skip it. Units disabled
and all four deployed files removed. The generic browser-candidate lists that
merely mention thorium are left alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The 316-line installer was 66 over the 250-line cap. The managed-copy helpers,
the /usr/local/bin install step, the integrity + drift manifest, and the
immutability/LeechBlock/VirtualBox payloads move into a sibling lib/, leaving a
128-line entry.
What stays in the entry is chosen by what the harness can execute, not by
taste. `--bind-abs /usr/bin` removes the directory bwrap needs to exec
anything, so the trace stops at `ln -sf ... /usr/bin/pacman` with exit 1 on
EPERM. That line onwards, plus the `if [ ! -L /usr/bin/pacman ]` backup branch
whose guard is false on this machine, therefore never run under trace — moving
them would have shipped code no diff could check. They stay put.
Everything else is proven: the before/after traces are identical, including all
16 generated-file hashes. pacman_wrapper at 548fc2b53ee4cea6 is the one that
matters most — it is post-`sed -i`, so it catches a reorder of the rewrite
relative to the copy.
The path definitions stay in the entry too, for a linter reason rather than a
runtime one: a definitions-only lib assigns without referencing, which is
SC2034 on 36 variables, and suppressions are not allowed. Libs that only
reference globals are fine. `export` would have silenced it while leaking 40
variables into every child process, which the trace cannot see.
Eight assertions across two test libs grep the installer's source text; five
needed widening to search entry+lib/. TEST 9's `chattr +i` grep caught the move
as a genuine failure first, which is what prompted enumerating the rest. No
assertion was weakened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The 290-line installer was 40 over the 250-line cap. Its five banner-marked
steps move into a sibling lib/, leaving a 53-line entry that owns log()/die(),
the globals that cross between steps, and the call order.
Proven by execution, not inspection: meta/scripts/trace_shell_split.sh
--prefix was captured at HEAD in place (tracing from a detached worktree
changes REPO_DIR, which is interpolated into a generated file, and fakes a
diff), then re-run after the split. The traces are identical -- same exit
status, same ordered stubbed calls, and all five generated-file hashes
unchanged, including usage-report-catchup.sh at sha=9f4803ab157c5527.
REPO_DIR is computed once in the entry and read as a global rather than
recomputed inside a lib one directory deeper; recomputing it would have
pointed the generated catch-up script at the wrong repo while still exiting 0.
test_usage_monitoring_installer_efficiency.sh greps the installer's source
text for the pmon heredoc, so it follows that heredoc to lib/nvidia_pmon.sh.
Its extraction anchor also had to become whitespace-insensitive: the
pre-commit hook runs `shfmt -w` on staged shell files and rewrites
`cat > "x" << 'SCRIPT'` to `cat >"x" <<'SCRIPT'`, which silently broke
extraction. All five property assertions are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
421 -> 218 + 157 + 75. The assertion helpers (pass, fail, assert_*,
strip_noncode, run_recon) go to lib/assertions.sh; the six
20-dump-stock.sh tests go to lib/dump_tests.sh. run_tests.sh keeps the
counters, the trap, the eight recon tests and main().
Verified by running the suite, not by inspecting it: 58 passed / 0 failed
before and after, and a diff of the complete 89-line output reports no
differences. The suite is hermetic -- every test drives a fixture via
MTK_ROOT_FIXTURE -- so this is a real execution of 58 assertions rather
than a stubbed trace.
All three files are shfmt-dirty, but run_tests.sh already was at HEAD
(2-space indent against shfmt's tabs) and the libs inherit that style, so
no clean file was made dirty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
test_pacman_wrapper_security.sh (287) and test_i3blocks_efficiency.sh
(345). Both keep their filenames -- shell-tests.yml runs them by name --
and both were verified by diffing a full run against the pre-split code:
byte-identical output, exit 0 either side.
The i3blocks setup half stays in the entry on purpose. Its five fake
$BIN_DIR binaries are written with heredocs, and a seam inside a heredoc
yields a lib that will not parse -- the worst failure this effort has
produced so far. Only the two heredoc-free regions moved.
Both i3blocks lib functions end in an explicit `return 0`. Their last
statement is a `|| fail ...`, which as a function tail under set -e
becomes the return value and would abort a run in which every check
passed. `fail` exits on its own, so reaching the end means success.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two files under the cap.
test_security_hardening.sh (423) keeps its filename -- shell-tests.yml
runs the suite by name -- and its five sections move into three sourced
libs. They are top-level blocks, not functions, so each is wrapped and
called in the original order; the counters and LIVE_SECTION stay in the
entry, which sourcing shares. Verified by diffing a full run against the
same suite from a detached worktree at HEAD: byte-identical output, same
exit status, still 18/1/5 on this host. The one failure is pre-existing.
Linting a lib alone turns a bare LIVE_SECTION=1 into SC2034, since its
only reader is test_result in the entry. A live_section() setter makes
the write a real reference, so no suppression was needed.
The mutation-driver split was already in the tree from the previous
session and did not run: the operators had moved to
_session_autopsy_mutations.py but the driver still named Mutation
without importing it, so mypy and pylint both reported undefined-variable.
Restored the import; the script now reports all 12 tests killed.
block_compulsive_opening.sh (705) is deliberately untouched and now says
so in the contract. install_all copies the running script to
/usr/local/bin, so an entry sourcing lib/ would resolve SCRIPT_DIR there,
find nothing, and leave beeper, signal-desktop and discord unlaunchable
with a failing pacman hook. Splitting it means teaching the installer to
copy the libs -- a refactor, not a line-count split.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
266 -> 204, with the warmed qalc coprocess (its four _CALC_Q* state vars
and four functions) in calc-live-qalc.zsh at 74.
The source line uses ${0:A:h}, matching what the file already does for
_CALC_EVAL_PY. That is load-bearing here: this file is loaded as
~/.oh-my-zsh/custom/calc-live.zsh, a symlink into the repo, so a naive
$0 would look for the sibling in the oh-my-zsh custom dir, where it does
not exist -- and break every new shell, not just one script run.
So it is verified THROUGH the symlink, not the repo path: sources clean,
functions defined across the seam, and the qalc path in the new file
answers end to end ("=5 ft to cm" -> 152.4 cm). Seven expressions give
byte-identical results to HEAD, and _calc_is_expr still accepts "2+2"
while rejecting "ls -la".
zsh -n is the parse check here; shellcheck cannot read zsh and the
repo's hook excludes it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
analyze_repo.sh was shfmt-clean at 133fccf and my split made it dirty
(a double blank line). Fixed with shfmt -w, then restored the STEP 1
comment banner shfmt -w also stripped -- all eight banners re-counted.
The three new shell_check libs inherited their parent's 2-space
indentation; as new files they have no prior style to preserve, so they
are now tabs like shfmt wants.
Context for the exit code: 157 of 298 linux_configuration shell files
already fail shfmt at 133fccf. shfmt is not in .pre-commit-config.yaml
and there is no .editorconfig, so run_linters exiting 1 on the full tree
is the repo's standing state, not a regression here. Scoped this commit
to what these splits touched rather than reformatting 157 files.
Also closes the risk left open in a84556a: run_linters now verified
directly over the three new libs -- all six passes PASS, exit 0 -- with
install_linters never invoked, so decision 6 holds. That is the path
.github/workflows/shell-check.yml runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
41cc045 passed bash -n, shellcheck, a function-set diff, a line-set diff
and a stubbed source run -- and still shipped a script that aborted after
language detection. Both bugs needed a real execution to see.
1. set -e. The eight `((LANG_FILES[x] > 0)) && HAS_Y=true` lines were
top-level statements, where a false `&&` merely left $? non-zero. As
the tail of detect_languages they became its return value, and the
last one (java, false for most repos) killed the script. Now `|| true`.
2. Self-referencing nameref. `local -n LANG_CODE_FILES="$1"` pointing at
a global of the same name makes bash 5.3 warn "circular name
reference" on every access -- into this script's own output. The
locals are now _code_files.
The rename then hid the array from shellcheck (SC2034), since it was only
named as a bare word. Fixed by passing it through CODE_FILES_MAP so the
reference is real, not by suppressing.
Verified against 133fccf end to end on a fixture repo: same file set,
every text artifact byte-identical, exit 0 both sides, zero warnings.
Only cscope.out.in differs, in binary padding -- same input list, same
90112 bytes.
The lesson for the recipe doc: a stubbed run sources the libs and never
calls them, so it cannot reach a bug in a seam that passes state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
428 lines -> 121, with lib/shell_check_lint.sh (171),
lib/shell_check_install.sh (86) and lib/shell_check_discover.sh (78).
The temp-file paths (TMPDIR, ABS_FILES_Z, REL_FILES_Z) are set once in
the entry before any lib is sourced and only read afterwards, so nothing
mutable crosses a seam. shellcheck was clean on the first attempt.
Runtime check is --skip-install --list-only --verbose, diffed against the
same run from a detached worktree at HEAD: 318 files before, 321 now,
the delta being exactly the three new libs the discovery walk now sees.
Nothing removed.
Not verified: the linting path itself. --skip-install does not gate the
AUR branch of install_linters, so a full run cannot be done under
decision 6 -- I learned that by running it, which installed four AUR
build deps before I killed it. The user chose to leave them. run_linters
moved as one intact block with no interior edit and the line-set diff
shows zero lines lost, so the seam risk is confined to its two braces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
412 lines -> 135, with the nine functions in lib/music_detect.sh (133),
lib/music_monitor.sh (91) and lib/music_status.sh (85).
find_music_services and kill_music_services stay together: the first
writes MUSIC_FOUND_PROCESS/MUSIC_FOUND_WINDOW and the second reads them,
so splitting between them would thread mutable state across files.
The test suite caught what the static checks could not. The harness
copies the script into a temp worktree to stub common.sh, and copied
exactly one file -- so after the split every source line failed and the
script died before any behaviour under test ran. Teaching it to copy
lib/ too is a harness fix under decision 8: no assertion changed, the
same five checks run, exit 0 before and after.
bash -n also earned its keep: the first splice dropped notify_user's
closing brace, and shellcheck reported that only as SC1094 "parsing of
sourced file failed" against the entry.
One rename, not a suppression: monitor_loop's local `music_services`
became `active_services` after shellcheck flagged SC2153 against the
MUSIC_SERVICES config array now visible in the same file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
869 lines -> 231, with the extracted helpers and per-STEP blocks in
lib/analyze_repo_{scan,tools,separate,report,symbols}.sh (86-228 each).
The seam that mattered was STEP 4, whose two passes share a map of
language -> temp file. Rather than let that cross a file boundary, the
map is passed by nameref and the comments file by parameter, so
separate_code_and_comments fills what report_languages reads with no
global in between.
Two boundaries were one step off on the first pass and produced 23
shellcheck findings: the KEYWORDS_* tables and detect_languages had been
cut away from their only consumers, so every one read as unused (SC2034)
or unassigned (SC2154). Moving each beside its consumer cleared all 23
without a suppression. The nameref also makes shellcheck read a bare
assoc-array subscript on the left of an assignment as arithmetic;
quoting the nine keys is the fix.
The line-set diff against HEAD earned its place here: it caught a
`find ... -printf` call at original line 719 that had fallen into a
one-line gap between two extraction ranges and been dropped. bash -n and
shellcheck both passed on that broken version.
Verified without running the script (decision 6 -- install_missing_tools
calls sudo pacman, cargo and makepkg): shellcheck clean across all six,
the function set matches HEAD plus exactly the six new wrappers, no
executable line lost, and a stubbed run of the entry preamble defines
all fourteen functions, proving the source lines resolve at runtime.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
569 -> 211 across three libs. All seven heredocs sit inside functions,
so function boundaries were safe seams.
The script guards its entry with SETUP_DNS_BLOCKER_SKIP_MAIN, which made
a real function-set comparison possible on a script that would otherwise
reconfigure the machine when sourced: an identical 74 names against a
detached worktree at HEAD. The dns-blocklist-refresh unit still resolves
to it and it still parses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
870 -> 146 across three libs, plus 247 lines of hosts entries moved from
a heredoc into data/android_guardian_blocklist.hosts. A data file draws
no linter, unlike extracting an embedded Python program.
The installed blocklist is byte-identical: md5 7d3f2550 on both sides.
The first attempt cut inside the `case` dispatcher and neither the entry
nor a lib parsed. Redone with the case as a hard boundary - this script
has no main(), so the case block and the argument parsing that feeds it
stay in the entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
503 -> 132 across three parts. The seven mtk_root consumers are
untouched because the path they source is unchanged.
Identical 28-function set before and after, checked after each of the
two carves. The `<<<` in this file are here-strings, not heredocs -
worth confirming rather than assuming, after enforce_vbox_hosts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
620 -> 163, with common.sh staying the single entry point that sources
its own three parts - the shape the handoff prescribes for config.sh, so
all 49 consumers are untouched.
Verified by diffing `declare -F` after sourcing it against the same from
a detached worktree at HEAD: an identical 48-function set. For a library
this widely sourced that is the check that matters, not a passing lint.
The FOCUS_APPS_* arrays moved with is_focus_app_running, the only
function that reads them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
raspberry_pi_flash_sd.sh 661 -> 166 and test_boot_repair.sh 315 -> 104.
Heredoc positions were checked before choosing seams this time.
test_boot_repair.sh is the first linux_configuration script in this
effort that could be verified by running it: every case builds a
throwaway root in a temp dir and passes --root. 31 passed after the
split, and 31 passed from a detached worktree at HEAD - a real baseline
comparison rather than trusting the post-split number.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
transcribe.sh 473 -> 39, setup_activitywatch.sh 457 -> 102 and
setup_wireguard_ssh.sh 464 -> 136.
enforce_vbox_hosts.sh is reverted: its seam landed inside a heredoc and
the resulting lib had an unterminated here-document. That is the worst
failure yet, because shellcheck reported it only as SC1094 on the caller
- which reads like a nicety, not a file that will not parse. The
verifier now runs bash -n over every lib before linting the entry and
names the offending file; proven by corrupting a copy and restoring it.
Also guarded a cd before `git clone ... .`, which would otherwise have
cloned an AUR repo into the caller's directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
setup_offline_docs.sh 713 -> 121 across six libs, carved one language at
a time since the first cut left offline_cpp at 545. optimize_arch_
desktop.sh 623 -> 118.
Two recurring traps again: a script with no SCRIPT_DIR at all, and libs
that chained rather than being sourced from the entry. Both now checked
for routinely.
libre_translate.sh is reverted and stays at 488. Exporting GENERATED to
fix one SC2034 revealed eighteen more - when a fix uncovers more of the
same, the seam is imaginary, not the fix wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
raspberry_pi_nextcloud.sh 1276 -> 163, root_bl9000.sh 1116 -> 211,
lookup_docs.sh 942 -> 165. Sixteen new libs.
lookup_python was one 300-line function; its keyword-to-anchor mapping
became python_keyword_doc, emitting result and desc tab-separated so the
caller keeps its control flow.
bl9000_mtkclient had cd X; work; cd - with three return paths.
install_mtkclient became a subshell; extract_boot_with_mtkclient could
not, since it exports BOOT_IMG - so the venv check moved before the cd,
the cd is guarded, and the restores use a saved $PWD.
Four more unguarded cd calls fixed, and a third instance of the lib
chaining bug flattened. lookup_docs.sh had no SCRIPT_DIR at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Eight libs from the largest script in linux_configuration: five in the
first pass, then three of those carved again since they landed at 272,
292 and 277.
Fixed three more unguarded cd /var/www/nextcloud calls, each right
before an occ command - the installer would have run against whatever
directory the caller was in. Third script with this bug.
check_and_enable_services.sh is reverted and stays at 1337. Its
SERVICE_STATUS array is declared once and written by every check_*
function, so any lib holding them fails SC2034 alone. Fourth file
deferred for shared state, and the clearest: the script is an
accumulator, so there is no seam anywhere in it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
prettier only runs on pre-push, so the split commit passed the per-file
gate and then failed the push. Both files stay under the cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Twelve prose violations to zero. Eight files split into per-topic
subdirectories with the original left as an index; both SKILL.md files
keep their frontmatter so discovery still works, with detail moved to
references/.
Deleted docs/cleanup-2026-05/ (7 files) and linux_configuration/docs/
VERIFICATION.md on the user's ruling: unreferenced records of finished
work whose rules are now enforced by the polling, shellcheck and shfmt
hooks. PACMAN_WRAPPER_SECURITY.md was split instead - unlike those, it
documents live guarantees.
Every split verified by a sorted diff against HEAD: zero content lines
lost, only the new headings and index links added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Four libs written in one pass rather than four sequential splits, which
is what stops the libs chaining - the entry gets one source block naming
all four and no lib sources a sibling.
steam_compatibility.sh is reverted and stays at 663. Its CACHE_MAP
associative array is declared and filled in what would be the ProtonDB
lib but read by main, so that lib fails SC2034 linted alone. Third file
deferred for the same reason: shared mutable state means the seam is not
there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Six libs: the script has four phases and write_stack alone was 342 lines
because it emits three config files from heredocs. That became three
functions, one per file, each keeping the comment that explains it -
including why settings.yml is mode 640 and not 600.
For a script whose job is writing config, "the functions still exist" is
not enough. Compared the sorted non-comment lines of the original
write_stack against its three successors: identical hash, so every line
of emitted compose, settings.yml and limiter.toml is byte-for-byte the
same and only the wrappers were added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
organize_downloads.sh 513 -> 72, setup_kcd2_dice_solver.sh 497 -> 112,
fix_ubuntu_performance.sh 494 -> 150. First 490+ line scripts done; all
three needed two or three seams.
Repeated splitting chained the libs - each sourced the next, because the
tool inserts its source line relative to the file it just wrote.
Flattened so the entry sources all three and no lib sources a sibling.
diagnose_pacman_hook_stall.sh is reverted and stays at 493. Its
transaction runner assigns LAST_ELAPSED that the caller reads and reads
PACMAN_BIN that the caller defines, so linted alone it is SC2034 plus
SC2153. The pre-commit hook lints each lib in isolation and fails, while
shellcheck -x on the entry passes - suppressing that would be papering
over a seam that does not exist.
organize_downloads.sh is the ExecStart of media-organizer.service; the
unit still resolves and the script still parses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
10-recon.sh, install_exercism.sh, fix_arch_performance.sh,
format_sd_card.sh, setup_dot_resolver.sh, fix_unity.sh and
20-dump-stock.sh, all now under the cap.
Splitting a file twice is the risky move: the second cut on
20-dump-stock.sh dropped the first cut's source line, leaving
verify_dump called but never defined, and the repair then landed the
line inside the lib instead of the entry script. Both fixed - the entry
sources both libs and no lib sources another. Grep the source lines
after any double split.
fix_unity.sh's desktop_dir is read across the seam; renamed to
DESKTOP_DIR to match the file's own convention for globals.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
install_unreal_mcp.sh 395 -> 155, diagnose_arch_performance.sh 377 ->
123 (two cuts), setup_local_llm.sh 370 -> 163, repo_to_study.sh 365 ->
160, setup_dwm.sh 359 -> 147.
The SCRIPT_DIR fix from the last batch held on all five. Fixed a second
unguarded cd, this time before the study-material generator ran - the
same latent bug class as build_website, surfaced because a moved
function gets linted in isolation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
setup_syncyomi.sh 408 -> 138 (two cuts), setup_personal_website.sh
396 -> 192, fix_thorium.sh 377 -> 208.
All three already declared SCRIPT_DIR and the splitter added a second
one, which aborts at runtime with "readonly variable" while bash -n and
shellcheck stay clean. The stubbed run caught it; the splitter now
reuses an existing declaration.
Fixed SC2164 in build_website rather than suppressing it: a bare
cd "$WEBSITE_SRC" inside a subshell, with pnpm install and pnpm run
build after it. A failed cd would have built in the caller's directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
control_from_mobile.sh 415 -> 180 and setup_periodic_system.sh 408 -> 228.
setup_periodic_system.sh has no main(): it defines functions then runs a
block of top-level calls, and the splitter moved that block into the lib,
changing when it ran. Redone by hand moving only definitions. It also
asks for sudo at source time, so the stubbed run is off-limits - verified
instead that the source line precedes the first call and all eleven
invoked functions are defined exactly once across the two files.
clean_audio.sh is reverted and stays over the cap. Two probe-result
globals are read from what would be two different libs, and renaming
them to satisfy SC2154 only moved the warning along. That is a refactor,
not a line-count split, and forcing it would have cost a suppression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
turn_off_auto_idle_screen_shutdown.sh 289 -> 180, mtk_root/install.sh
262 -> 141, setup_gitea.sh 255 -> 198. A deliberately small batch to
check jscpd before scaling; it stayed well under 2%.
The stubbed run earned its place again: mtk_root/install.sh aborted with
"SCRIPT_DIR: readonly variable" because it already declared one, and
both bash -n and shellcheck were clean on that broken file.
Two shellcheck findings the split exposed are fixed at the source, not
suppressed: persist_with_systemd_logind now takes the flag as an
argument instead of reading the caller's global, and a dead python_bin
assignment is gone. Neither was introduced here - the pre-split file
already had three SC2034 warnings.
No script was executed; all three are installers or enforcement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
299 -> 103, with the --with-server half in lib/joplin_server.sh. First
of the 84 linux_configuration files, done slowly to settle the pattern.
Three things bash -n and shellcheck do not catch, all found here:
- SCRIPT_DIR from "$0" breaks when the entry script is itself sourced.
Use "${BASH_SOURCE[0]}"; keep readlink -f for the one symlinked script.
- A lib needs a shebang AND +x: without the shebang shellcheck raises
SC2148, with it the shebang-executable hook fails. Match common.sh.
- Only a stubbed run proves the source line resolves. Copy entry+lib to
a mktemp -d, sed `main` into a declare -F probe, run that. No
installer step executes.
Also enumerates which scripts escape the repo: three copied out by
install_pacman_hooks.sh, one symlink, everything else generated. All
recorded in docs/shell-split-recipes.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
CLAUDE.md's longest and most emphatic code rule -- never embed Python program
logic in a shell script -- was enforced by nothing. A repo-wide scan found it
had already been violated: install_plagiarism_tools.sh ran a 6-line NLTK
download as an inline `python3 -c`, invisible to ruff, mypy, pylint, bandit
and pytest, exactly the failure mode the rule exists to prevent.
check_no_inline_python.sh flags heredocs and multi-line `python -c` while
permitting the single-line availability probes the rule explicitly allows, so
the ~250 shell files here produce zero false positives. Commented-out examples
are skipped; otherwise the rule's own documentation would fail the hook.
The extracted download_nltk_data.py also fixes a real defect: the inline block
ignored nltk.download()'s return value, so a partial corpus download reported
success. It now returns non-zero and the caller's `set -e` sees it.
nltk is imported via importlib inside main(), not at module scope: it belongs
to the plagiarism toolchain, not this repo, so it is absent from
requirements.txt and unresolvable to pylint as a static import.
No suppressions anywhere: T201 avoided with sys.stderr.write, INP001 with a
shebang (matching sibling fast_count.py), E0401 with the dynamic import.
pylint 10.00/10, ruff clean, mypy clean.
Verified the gate fires rather than passing vacuously -- a staged canary with
a real heredoc exits non-zero -- and that the extracted script runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
install_ublock.sh patches the same wrapper install_leechblock.sh already
targets, always rewriting --load-extension into a single comma-joined
flag rather than appending a second occurrence — verified empirically
that Chromium is last-wins across separate --load-extension flags and
would otherwise silently drop LeechBlock.
seed_ublock_storage.js previously hardcoded uBlock Origin's Chrome Web
Store extension ID, but --load-extension (unpacked) loads get a
synthetic, install-path-derived ID instead. It now reads the real ID
back out of the profile's own Preferences file, with a --ext-path
override for installers whose package path differs from Arch's.
install_ungoogled_chromium_ubuntu.sh's UBLOCK_EXT_PATH was an unverified
guess (/usr/share/webext/ublock-origin-chromium); corrected to the real
Debian/Ubuntu package path confirmed via packages.ubuntu.com.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yfDYbaw1D1WhVMqWhsSDA
is_blocked_package_name compared raw pacman -U <file>.pkg.tar.zst
arguments against whitelist entries, which never matched since yay
always installs AUR packages this way. Added strip_pkgfile_suffix()
to derive the bare package name first, plus a regression test.
Also swaps the browser whitelist entries to ungoogled-chromium
(Chromium 151 dropped MV2, breaking uBlock Origin's full/classic
build).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yfDYbaw1D1WhVMqWhsSDA
The network layer was the wrong place for this. Tested remotely today: opening RethinkDNS and switching off its device blocklists un-blocked YouTube in a few taps, and no device owner API can stop that -- an on-device filter app always owns its own rules.
App hiding is different. 'pm unhide' from adb shell throws a SecurityException and the package stays hidden; only the device owner can reverse it, and reaching that costs a factory reset. So the block moves there, scoped to an explicit allowlist and the home geofence.
Both whitelists are rewritten from the apps that actually exist on this phone. The old ones described the rooted Blackview. Every browser is excluded now, so at home a link does not open at all -- confirmed as intended.
LAUNCHER_PACKAGE was still the Blackview's launcher, which is not installed here. The exporter refused to render the policy twice over it -- precisely what that check is for.
The network layer stays a soft deterrent by choice: pinned and uninstall-blocked, but without lockdown, and its blocklists remain switchable.
Private DNS is implemented but ships disabled. stunnel logged zero connections from the handset across two pin attempts, so the router does not forward 853.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC
The claude_usage i3blocks tests failed on CI while passing locally.
archlinux:base-devel ships no jq, and claude_usage.sh parses its cache
with it, so the block degraded to its "no data" path and every
percentage assertion failed. That degradation is correct at runtime —
the bar should not break because a tool is missing — but it made a
missing dependency look like a logic bug.
Install jq alongside git and strace, and probe for it at the top of the
section so the failure names its own cause. The probe runs jq rather
than calling `command -v`: a jq that exists but cannot execute produces
exactly the same "no data" output as an absent one, and `command -v`
passes in that case — verified by stubbing a jq that exits 127.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
fee[dB]ack has no AUR entry, so install_feedback.sh builds a local
package from the PKGBUILD tracked beside it, the same way
hypersomnia-appimage and jackify-bin are installed here. The script only
builds and then prints the `sudo pacman -U` line to run, since
interactive sudo prompts belong to the user rather than an agent.
The built .pkg.tar.zst is ~640 MB, so it is gitignored along with the
Bottles prefixes and the archwiki_rag vector index — all three are local
runtime state that is regenerable and must never enter the history (see
memories/binary-files.md). The PKGBUILD and the installer are the
source and stay tracked.
Exclude PKGBUILDs from shellcheck. A PKGBUILD is a makepkg manifest,
not a standalone script: it has no shebang by design, pkgname/pkgver/
source/sha512sums are read by makepkg, and pkgdir/srcdir are injected by
it, so shellcheck flags every one as unused or undefined. The same
reasoning already excludes zsh — the dialect is outside what shellcheck
can judge — and the alternative was a directive on nearly every line.
Also drops three stray files: a saved search-results page, a 0-byte file
from a mistyped redirect, and a June i3 config backup superseded by git.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
TachiyomiSY 1.13.2 can report a restore as SUCCESS while writing almost
nothing, then sync that near-empty library up and replace the only good
copy. On 2026-08-09 a 14 MB payload holding 2182 manga became a 267 KB
stub, and the real library survived only as stale pages in the SQLite
write-ahead log.
The upstream bug is Kotlin in an APK and cannot be fixed here, so this
targets the fixable half: decode the payload, count what is actually in
it, and refuse to treat a collapse as normal drift. A pure is-it-zero
check would have caught this incident but not the harder case upstream
describes, where a restore keeps some entries and drops the rest, so the
threshold is proportional. Categories get their own rule because all
2182 manga survived the round trip and all 18 categories did not.
Adds the setup script and a runbook covering the WAL-page salvage used
to recover the library on the day.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
Add a [claude_usage] block showing the 5h and 7d rate-limit windows. It
reads ~/.claude/cache/limit-state/*.json, which Claude Code's statusLine
hook already writes to disk, so the data costs nothing to display: the
script only runs jq against a local file and makes no API call. Limits
are account-wide while the cache is sharded one file per project, so it
takes the newest file and ignores cwd. Percentages arrive as floats and
are truncated; a stale cache is labelled rather than shown as current,
and a rolled-over window reads "?%" instead of a misleading value.
Replace Font Awesome private-use-area icons with plain Unicode. Both
fonts named in the bar's pango string ("System San Francisco Display",
"FontAwesome") are not installed, so PUA codepoints fell through to
whatever font claimed them: ethernet rendered as a star, wifi as
Cyrillic, gpu as Hangul, disk as a chess pawn. Emoji resolve through
fontconfig consistently, which the pre-existing volume block already
relied on, and need no change to ~/.config/i3/config.
Remove the bluetooth, battery, wifi, activitywatch, warp and
network_monitor blocks: this desktop has no BT adapter, no battery, no
wireless interface, and no warp-cli. This also removes a rendering bug,
since activitywatch_status.sh and warp_status.sh were interval=persist
yet emitted three lines per update, leaking their colour line into the
bar as literal "#FF0000" text.
Fix ethernet reporting "down" on a live wired link:
find_ethernet_interface returned the first non-loopback interface, which
with docker installed is a br-* bridge (permanently down, and sorted
before enp*). It now requires a real /sys/class/net/*/device and prefers
an interface that is up.
Tests drop the assertions and fixtures for the removed blocks and add
claude_usage coverage (fresh, warn, critical, stale, rolled-over, null
reset, unparsable, missing dir), a guard that removed blocks cannot
return, a repo-wide check that no block script uses a PUA codepoint, and
an ethernet physical-NIC regression check. The fork budget is asserted
against a populated cache directory, which caught a stat call inside the
scan loop that cost one fork per cached project (170 -> 2 execs).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
Thorium was "closing and dying randomly" with no evidence anywhere.
Two independent structural causes, both now fixed:
1. Crashpad was never enabled -- no crash_reporting consent key and an
empty crash DB, so no minidump was ever written.
2. stderr from a dmenu/.desktop launch reaches no terminal and no
journal. The only Thorium lines ever seen in the journal came from a
systemd-launched instance, which is why they survived.
browser-preexec-wrapper.sh now gives every Chromium-family launch its own
log under $XDG_STATE_HOME/browser-crash-logs: a VRAM/RAM/kernel/driver
snapshot before start, the browser's stderr, and on exit the exit code,
decoded signal, and a SECOND VRAM snapshot. Those two snapshots plus the
signal are the discriminator between the live hypotheses -- a VRAM death
shows NVRM/NV_ERR_NO_MEMORY or near-full VRAM at exit, a stale-binary
death shows SIGSEGV with the GPU idle. Root cause is deliberately NOT
asserted here; the next crash decides it from evidence.
Three details that are load-bearing rather than incidental:
- The exit record is written from a signal trap, not inline after
`wait`. If the wrapper itself is killed, an inline block never runs
and the log ends mid-stream with no verdict -- indistinguishable
from a browser that is still running.
- The browser is a direct redirect, NOT a pipe into a size filter: in
a pipeline `wait` reports the last element's status, discarding the
browser's signal, which is the one fact worth capturing.
- Logs are capped by bytes as well as count. Measured: ~2 MB per 45s
of navigation (65% VERBOSE1 net/TLS chatter), ~0 while idle. Count
alone would not bound disk use, and a full disk causes exactly the
random deaths under investigation.
setup_periodic_system.sh: rewrite Chromium "app mode" .desktop entries
(Exec=/opt/<browser>/...) to go through /usr/local/bin. These are
auto-generated and bypass the wrapper entirely, so neither the flags file
nor the logging applied to them -- five Thorium entries were found still
launching without the --disable-gpu-compositing mitigation added on
2026-07-25, two weeks after the fact. Runs on every install because
Chromium regenerates the files.
pacman_whitelist.txt: allow the exact package name `librewolf`. The
`librewolf` keyword stays blocked, as do all other Firefox forks. It is
LeechBlock-enforced via /etc/librewolf/policies (force_installed), so
this is not an unfiltered escape hatch; it is needed because Chromium's
MV2 sunset ends full uBlock Origin with all filter lists, which the Gecko
build keeps.
Verified: shellcheck clean, no lint suppressions; fake-browser SIGSEGV
and wrapper-SIGTERM both produce a verdict; trim watchdog bounds a
runaway log; kill -SEGV on the real /opt/thorium-browser/thorium wrote
"exit code: 139 / died on signal: 11 (SEGV)"; librewolf installs and
loads with uBO + LeechBlock active.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME5ZkgWjFvPZrUwMpVZM3e
The previous commit's pruning did not actually take effect. Per enginelib,
"disabled" only sets the DEFAULT for a fresh visitor -- "will allow the user
to manually activate it in the settings" -- so any browser session with an
existing preferences cookie kept running every pruned engine. yandex images
was still throwing JSONDecodeErrors in the log minutes after being
"disabled". "inactive" is the key that actually removes an engine.
Converted 84 entries. Verified behaviourally rather than by reading config:
an images search now serves from google cse images ALONE (20 results,
unresponsive=[]), and no pruned engine appears in the logs after a restart.
brave and qwant deliberately keep "disabled". Sibling engines reference
their outgoing connection pool by name (network: brave <- brave.images/
.videos/.news; network: qwant <- qwant news/images/videos), and "inactive"
removes the definition entirely, so the container dies at startup with
KeyError: 'qwant'. "disabled" keeps the pool defined while hiding the engine.
Pool owners in this image: brave, qwant, yandex, piped, yacy.
Also fixes two status checks that could report a healthy instance as broken.
Public /search was returning 429 while loopback returned 200, which looked
like a regression but was a missing Accept-Language header in the request:
botdetection runs one check per header and rejects on the first failure.
Setting "debug: true" under general: makes SearXNG log the exact rejecting
method ("NOT OK (searx.botdetection.http_accept_language)"), which settled it
in one run after several wrong theories. wait_for_site and check_headers had
the same omission and are fixed; the full required header set is documented
at check_http.
Public path confirmed working end to end: general 200 with 21 results in
0.71s, images 200 with 40 proxied thumbnails in 0.58s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG
kuhy went through the /preferences engine table and selected every erroring
engine (CAPTCHA / access denied / timeout / parsing error / crash) plus the
slow tail, across all nine categories. Applied all of them, keeping only
google cse images for the images category as requested.
Result: unresponsive_engines is now [] in EVERY category. Per-category
latency (q=linux): general 572ms, images 406ms, videos 1378ms, news 455ms,
music 461ms, it 1698ms, science 1338ms, files 584ms, social media 1074ms.
Images went 2.1s -> 406-900ms across 12 queries, still 20 results. The root
cause was not google cse images being slow (1.1s): SearXNG waits for the
SLOWEST engine in a category, so quark images at 2.9s set the floor. Cutting
the others removed the ceiling.
Three engines that were not in the list surfaced as unresponsive only under
load and were pruned with the rest: brave.images, startpage images, kickass.
Corrects a wrong claim from the first pass. I had said general search retained
three working sources because !go returned 20 results and !bi returned 10.
Upstream marks both google and bing "inactive: true", so they answer ONLY to
an explicit bang and were never part of the merged view -- which google cse
has been serving alone all along. The pruning therefore removed only engines
already contributing zero, but general search does have a single point of
failure, so status now prints the !go/!bi fallback when a live search returns
nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG
Deploys a SearXNG metasearch instance behind the existing shared Caddy edge.
Graded A+ (score 120, 10/10) by the MDN Observatory API, and "Vanilla" in the
sense that every change is settings-only -- no template or static-file edits.
Public but deliberately not submitted to the searx.space directory.
Both containers run network_mode: host because Docker bridge networking has
zero egress on this box (nftables' default-drop forward chain is terminal at
NF_INET_FORWARD regardless of Docker's legacy-iptables ACCEPTs), so a stock
bridge compose cannot reach a single search engine.
Non-obvious things established empirically against the pinned image, all of
which cost a debugging cycle and are commented at their use sites:
- granian binds :: (all interfaces) by default, NOT the 127.0.0.1 its own docs
claim; with host networking that would have LAN-exposed the instance.
GRANIAN_HOST/PORT are the only things pinning it. settings.yml's
server.bind_address is inert (read only under __main__).
- Caddy's `> Header "v"` is a substring-replace, not a header setter -- it
silently set nothing. The bare name is "set"; `+` appends and would duplicate
the headers SearXNG already sends (X-Content-Type-Options, Referrer-Policy).
- server.public_instance force-enables link_token, a browser-proof challenge no
scripted client can pass, which makes the JSON API unusable. Dropped it and
kept limiter/image_proxy via their own keys.
- Without limiter.toml there are no trusted_proxies, so botdetection 429s every
request including the homepage.
- The entrypoint chowns config to uid 977 and needs DAC_OVERRIDE to read it, so
cap_drop breaks startup and mode 600 breaks the worker's read (640 is right).
- `compose up -d` is a no-op when only a bind-mounted config file changed, so an
explicit restart is required or every check tests the previous config.
- All curl checks must send browser-shaped headers; bare curl gets 429, which
had the status command reporting a healthy instance as broken.
Engine pruning is measured, not guessed: brave/duckduckgo/startpage CAPTCHA on
100% of queries, flickr is the 1.0s tail, and pinterest was failing because
this host's own /etc/hosts blackholes it -- a local cause that would otherwise
have been misdiagnosed as a dead upstream.
Measured latency, reported honestly: front page median 19ms, but search median
486ms (233-623ms). Sub-200ms search is not achievable by any configuration --
a metasearch cannot return faster than the slowest upstream it queries. The
~200ms figure on searx.space measures initial front-page response, which is met.
Image pinned by digest: the workarounds above are only known-true for 2026.8.4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb77nxXGwFYFM4fyXEQkzG
Measured on the Pixel 6a, twice: with private_dns_mode=hostname and the
resolver unreachable, `ping example.com` returns "unknown host". Android's
Private DNS does not fall back to the carrier's resolver -- hostname mode is
RFC 8310's strict profile, and failing to establish TLS on 853 is a hard error.
So a resolver living on this PC means "PC off" equals "phone has no internet",
not merely "no filtering". That is disqualifying for a daily driver, and it
disqualifies the whole home-hosted delivery path regardless of how well it
works while the machine is up -- which it does; it was verified end to end.
DNS_TRUSTED_DOT_HOST/IPS go back to empty, which restores the previous enforcer
behaviour byte for byte. No code revert is needed, which is exactly why the
flag was built to default empty.
Two things found while testing are worth keeping. The nftables input chain is
policy drop and admitted only ssh over wg0, so binding stunnel to the tunnel
address was not sufficient on its own: ICMP passed while TCP 853 was dropped,
which presents confusingly as "ping works but the port does not". That rule is
now added explicitly and scoped to wg0. And setup_phone_wireguard.sh automates
the client setup the tunnel needed, verifying handshake, reachability and DNS
rather than assuming success, and deleting the pushed private key afterwards.
The deeper reason the home endpoint could not work even with the tunnel up:
dns.kuhy.duckdns.org resolves to the public IP while stunnel listens only on
10.8.0.1, and with AllowedIPs=10.8.0.0/24 that routes outside the tunnel to a
closed port. Android never opened a connection at all. Split-horizon DNS does
not fix it either, since on mobile data the phone uses the carrier's resolver.
Research also ruled out the hosted-hybrid option that looked most attractive:
no provider can ingest a 185k-domain list (NextDNS none, AdGuard 1-5K, ControlD
10K), and the free tiers fail OPEN -- they stop filtering past 300k queries a
month while still resolving, which for a self-control tool is the worst failure
mode available. Details and the remaining options are in the parked doc.
The stunnel endpoint is left running deliberately: WireGuard-only, verified
unreachable from LAN and WAN, and it makes the next attempt testable
immediately. Removal commands are documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
Two gaps stopped the unrooted path working end to end. The phone had no way to
read the focus policy, and the blocklist was only reachable as a hosts file or
over plain UDP/53 on the LAN -- while the rooted enforcer actively rejected
every DoT connection, so a self-hosted resolver could not have worked even if
one existed.
focus_owner now loads a generated policy.json instead of hardcoding anything,
so config.sh stays the single source of truth across Python, shell and Kotlin.
The committed asset is rendered with --redact-home and a test fails if home
coordinates ever appear in it. An unknown schema_version is rejected outright:
silently accepting a newer schema means enforcing a policy the app has misread,
and a misread allowlist is one that blocks the dialer.
dns_enforcer.sh rejected ALL port 853 traffic and pinned Private DNS off, every
20 seconds. That is right when DoT is a bypass channel and exactly wrong when
DoT is the enforcement channel, which is what an unrooted phone needs -- it has
no hosts file to fall back to. With DNS_TRUSTED_DOT_HOST set it now ACCEPTs the
trusted resolver before the blanket REJECT, and pins Private DNS to that
hostname so the user cannot pick an unfiltered one. Rule order is load-bearing:
iptables takes the first match, so an ACCEPT appended after the REJECT would
never be reached. expected_rule_count() counts the new rules, because a
mismatch there makes chain_intact() rebuild the chain every 20s -- a silent
fork storm that also drops the connection each time. With the variable empty
the behaviour is byte-identical, so the rooted phone is untouched until this is
deployed.
setup_dot_resolver.sh terminates DoT with stunnel rather than Caddy: DoT is a
raw TLS stream and stock caddy:2.8 has no layer4 module, so using Caddy would
have meant an xcaddy rebuild of a container serving five live sites. It reuses
the certificate Caddy already renews, so nothing competes for port 80. It binds
to the WireGuard address, not 0.0.0.0 -- a public DoT endpoint is an open
resolver, and over the existing tunnel it needs no port-forward, no firewall
rule and no rate limiting. Verified: example.com resolves, pornhub.com returns
0.0.0.0, TLS 1.3 against the LE cert, and both the LAN and public addresses
refuse.
The Device Owner checklist is planning only. Checking the device rather than
the config settled the one open risk: Google Wallet is not installed and no NFC
payment default is set, so the unconfirmable tap-to-pay question does not apply
here. The same check found that Aegis, MS Authenticator, Oracle IDM and PKO IKO
are absent -- they are in config.sh's whitelist, which describes the rooted
phone, not this one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
phone_focus_mode is built for a rooted phone: the hosts layer is a read-only
bind mount over /system/etc/hosts, chosen so that even root-in-a-terminal
cannot overwrite it. None of that ports to a stock locked device. This splits
out the parts that are device-independent so one policy can drive both.
python_pkg/focus_policy parses config.sh rather than copying it, so the shell
config stays the source of truth and the two cannot drift. It rejects the
shipped REDACTED_* coordinate placeholder instead of defaulting to (0,0),
which would move "home" to the Atlantic and quietly disable the location gate.
blocklist.py converts the hosts file to domains for a DoT resolver or a local
VpnService. Three properties of the real 194k-line file make the obvious parser
wrong, and all three are handled: generate_hosts_file.sh *comments out*
deliberate unblocks rather than deleting them, so stripping '#' re-blocks
facebook/messenger/linkedin/4chan; the StevenBlack header carries loopback and
IPv6 housekeeping that would blackhole localhost; and ~1060 upstream annotation
tokens ('#[adware.zeno]', '"spam') render as address=/"spam/, which makes
dnsmasq refuse to start -- taking DNS down rather than blocking anything.
Verified with dnsmasq --test over all 185681 generated domains.
strip_workout_hosts.py had the same class of bug on the live rooted phone:
exact set membership meant the allowlisted googlevideo.com never matched the
dynamic r*---sn-*.googlevideo.com hosts YouTube actually streams from, so the
workout unblock was only partly working. Now suffix-matched (17 lines stripped,
was 14). The three new tests were checked against the original code in a
throwaway worktree; two of them fail there, so they pin the fix rather than
passing vacuously.
focus_owner is a device-owner app that deliberately does nothing yet. It ships
clearDeviceOwnerApp() implemented and tested first, because dpm
remove-active-admin does not work on a device owner: an app that locks the
device down before proving it can let go leaves a soft-brick recoverable only
through fastboot on a locked bootloader. Built, installed and launched on the
Pixel 6a; dumpsys afterwards confirms Device Owner Type -1 and no user
restrictions, i.e. the phone is unchanged.
Measured while scoping this, and not yet reflected in any enforcement code:
on Android 16 both pm suspend and pm disable-user are cleared by a reboot, so
shell-level app blocking is per-session only. setApplicationHidden is assumed
to persist under Device Owner but is untested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
23fdfcf pinned shellcheck because a version split between CI and developer
machines kept the workflow red for a day. The same trap was still open for
every other `language: system` tool, so close it for all of them.
The risk is not identical across the four, and the code says which is which:
shellcheck, zsh run in CI. Drift shows up as a CI failure the developer
cannot reproduce -- the b83ce5d case exactly.
prettier, shfmt never run in CI (pre-push and local git hook). They
REWRITE files, so two developers on different versions
reformat the repo back and forth.
check_shellcheck_version becomes a table-driven check_pinned_tool_versions
over PINNED_TOOL_VERSIONS, and _tool_version normalises the four different
formats these tools report (shellcheck's `version:` line, shfmt's leading v,
zsh's second field). zsh is compared on major.minor only: apt cannot select a
patch release on the runner image, so 5.9.2 vs 5.9 is not a real divergence
and must not become a permanent warning.
CI gains a step installing prettier at an exact npm version and shfmt from a
checksum-verified release binary, each asserting what PATH actually resolves
to afterwards. Those two are unexercised by the current job by design -- the
pin records the expected version in one place and protects any future
workflow that does run them.
Verified: YAML parses with all pins ahead of the hook step; the shfmt install
ran verbatim with `sha256sum --check --strict` reporting OK and the version
gate passing; prettier@3.9.5 resolves from the registry; the drift warning
fires per-tool against stubbed prettier 3.0.0 and shfmt v3.0.0 (confirming v
normalisation and that it reports every drifted tool, not just the first) and
stays silent against the real toolchain; common.sh is shellcheck clean after
rewording a comment that began "# shellcheck, zsh --" and was being parsed
as a malformed directive; 1193 Python tests at 100% coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7
Root cause behind the outage 9dd9aeb cleaned up. The workflow never installed
shellcheck: it inherited whatever ubuntu-latest shipped (0.9.0), while
developer machines get 0.11.0 from pacman. The hook is `language: system`, so
each side linted with a different binary, and the two disagree -- 0.11.0
demoted SC2002 and several SC2015/SC2119 cases as noise. A commit could pass
every local gate and then fail CI on findings that could not be reproduced
locally. That is what kept the workflow red from b83ce5d onward, and why it
took fetching shellcheck 0.9.0 by hand to see the 11 findings at all.
CI now downloads shellcheck v0.11.0 from the official release, verifies it
against a recorded sha256, installs it, and asserts the version that PATH
actually resolves to. That last check matters: without it a stale copy earlier
on PATH would silently win and reproduce the same class of confusion.
The local hooks gain a matching EXPECTED_SHELLCHECK_VERSION and warn when the
installed version differs, naming both files to update. Warn, not abort --
shellcheck comes from pacman on a rolling distro, so a bump is routine and
should not block a commit; what must not happen is the disagreement going
unnoticed.
Verified: workflow YAML parses with the pin ahead of the hook step; the
install steps run verbatim locally with the checksum reporting OK and the
version gate passing; the drift warning was observed firing against a stubbed
0.9.0 and staying silent against the real 0.11.0; common.sh is shellcheck
clean; 1193 Python tests at 100% coverage. Pre-commit checks on 9dd9aeb went
green -- this change protects that rather than producing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7
"Pre-commit checks" has failed on every push since b83ce5d removed
shellcheck's --severity filter. The findings never appeared locally: CI
installs an older shellcheck than this machine, and 0.11.0 no longer emits
SC2002/SC2015/SC2119 at all. Reproduced by fetching shellcheck 0.9.0, which
matches CI's output exactly, and verified against both versions.
SC2015 x6 -- rewrote `A && B || C` as an explicit if. Each keeps its trailing
`|| true`, so a failing side effect still cannot abort the caller under set -e.
The first draft dropped it in run_phone.sh; a review pass caught that and it
was restored, since that call is the last statement of a function.
SC2119 x3 -- require_imagemagick and adb_select_device take an OPTIONAL first
argument, so the bare calls were already correct. Following shellcheck's
literal advice and passing "$@" would forward the script's own arguments as a
tool preference, which would be a real defect. Passing an explicit "" states
the same intent without changing behaviour, confirmed by running both forms.
SC2002 x2 -- `cat FILE | tr ...` becomes `tr ... < FILE`, keeping 2>/dev/null
so a missing file still yields an empty string.
No suppressions, no --severity filter, no exclusions added. Verified: 0
findings under both shellcheck versions, `pre-commit run shellcheck
--all-files` passes, test_adb_common 13/13, test_boot_repair 31/31, 1193
Python tests at 100% coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7
Prep for a Ulefone Armor X12 Pro (MediaTek MT6765) that has not arrived,
validated read-only against the Pixel 6a that has.
Partition discovery never hardcodes boot vs init_boot. It reads
/dev/block/by-name/, derives the slot scheme from observed _a suffixes, and
picks the ramdisk carrier by preference order init_boot then boot over what is
actually present. The AOSP launch-version rule explains why that ordering is
right; the code consults the filesystem, not the API level. One path therefore
serves both a Pixel (no init_boot, falls to boot_a) and a 13-launch MediaTek
device (init_boot_a taken). Patching the wrong one yields a phone that will
not boot.
Classification refuses rather than guesses: ULEFONE needs a positive MediaTek
signal AND a vendor match, with no "not a Pixel therefore Ulefone" fallback.
Carrier lock is always reported UNDETERMINED, because sys.oem_unlock_allowed,
ro.oem_unlock_supported and settings global oem_unlock_allowed read identically
whether a device is carrier-locked or merely has the toggle off; asserting a
negative would wrongly rule out a path.
No flashing script, deliberately. Automating irreversible partition writes
defeats the confirmation steps that catch a wrong partition or missing slot
suffix.
Three bugs found by running the code rather than only linting it:
- SIGPIPE: `adb version | head -1` under pipefail exited 141 and killed
00-preflight.sh mid-run
- 20-dump-stock.sh exited silently when no recon had run, because find on a
nonexistent cache dir fails under set -e
- the read-only static scan had a hole: stripping all quoted strings to avoid
false positives on printf text also hid `adb shell "reboot bootloader"`.
Now only printf/echo/cat arguments are stripped, and a negative self-test
asserts the scan fails on injected commands
Also narrowed a world-writable udev catch-all (idVendor=="*", MODE="0666")
that granted every local process read/write access to every USB device;
commented with a backup, adb connectivity re-verified afterwards.
Validated: 58/58 offline fixture tests; 00-preflight.sh HOST READY (12/12);
10-recon.sh read-only against the Pixel reports PIXEL, boot_a, A/B, no
init_boot, path C-or-D indeterminate, with the device unchanged afterwards.
Untested and labelled as such in READY.md: the init_boot branch on real
hardware, mtkclient over BROM, and the whole flash/verify sequence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:
- Five scripts in single_use/misc/testsAndMisc-bash/ sourced
$SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
at that path, so all five failed at runtime.
- setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
(which ends in `exit 0`) and then `exit 1` for an unknown command. The
exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
- Six i3blocks scripts had a `# shellcheck source=` directive missing the
scripts/periodic_background/ path segment. The stale path had silently
disabled source-following for all of them.
- diagnose_arch_performance.sh word-split a newline-joined file list; when
the find came back empty, awk got no file operands, fell through to
reading stdin, and the diagnostic hung.
- lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
`return 0` while documenting "Returns: 0 if handled" -- a contract no code
path could satisfy.
- deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
level, and config.sh can repoint it at the on-device path
/data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.
The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.
17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.
Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.
The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.
third_party/ is excluded -- vendored upstream, matching the prettier hook.
Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pacman intermittently freezes at its FIRST PreTransaction hook - 5 abandoned
transactions since 2026-07-29, stalls of 4 minutes to overnight, always fine on
retry. This investigates it and repairs the real defects found on the way.
The stall is NOT fixed, and deliberately so: no verified defect was found, so
nothing pacman-side was changed.
- Not guard-lib. /etc/guard-lib/targets was empty, so that hook script
provably exits in milliseconds. The failures share POSITION (first hook of
the transaction), not identity: one stall hit the hosts hook back when that
one ran first.
- Not reproducible on demand. 65 real transactions (40 idle, 25 under 2.6 GB
of memory pressure) produced zero stalls. Against the historical 4-in-56
rate that is ~0.9% likely, so the trigger is conditional on something a
synthetic load does not recreate. Memory pressure is untested at the load
that actually occurred - not refuted.
So diagnose_pacman_hook_stall.sh drives real transactions with a watchdog, and
its --watch mode passively captures /proc/<pid>/{stack,wchan,syscall}, the
process tree, meminfo and PSI from the next REAL stall. No pacman-side fix
should ship before that dump exists.
Three real defects were found and fixed:
1. The deployed pacman wrapper was 19916 B against a 27510 B source, never
sourcing the shared pacman_lock_lib.sh that makepkg_wrapper already used.
It hid because check_pacman_wrapper() only tested that files EXIST. Both
installers now write a drift manifest covering source AND installed hashes;
the checker replays it with sha256sum -c and fails closed when it cannot
verify. Manifest paths are absolute - relative ones resolve to nothing under
systemd's cwd=/ and would have looped reinstalling forever.
2. The guard-lib migration (66c4698) was never finished here: guard-lib was
installed with zero registered targets while the legacy hosts-guard layer did
all the work, so every transaction ran two PreTransaction hooks and the first
one did nothing. migrate_hosts_guard_to_guard_lib.sh registers hosts,
nsswitch and resolved, retires the legacy hooks and units, and has a
--rollback that was actually exercised. Plugins install to a stable path
because guardctl records absolute plugin paths. Side effect: one hook pair
instead of two, transactions ~5-6s -> ~2-3s.
3. "One heavy job at a time" was a rule a human had to remember. heavy_job_lock.sh
makes the overlap impossible: one system-wide flock shared between root and
the user (a per-user lock would give root's pacman and the user's builds
separate locks and no exclusion at all). Wired into pacman TRANSACTIONS only
- read-only queries stay instant - plus makepkg_capped and the coverage hook.
Always fails open.
Two bugs surfaced only by running --rollback, both of which briefly left
/etc/hosts with no immutable flag: fg_uninstall clears +i on its way out while
legacy watchers only fire on PathModified, and `((i > 20)) && break` left the
loop body's status at 1, which under set -e aborted the rollback silently - but
only when the loop actually ran, so --dry-run looked healthy.
Not migrated: the shutdown-schedule guard (/etc/shutdown-schedule.conf does not
exist here). Not wired: Flutter/Gradle builds, which have no single entry point.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings the repo to prettier-clean across all 525 tracked md/json/yaml files.
The markdown edits are cosmetic: escaping `L*` so it stops rendering as
emphasis, normalising `*em*` to `_em_`, and aligning tables.
billsplit/android/gradle.properties shipped from the Flutter template asking
for `-Xmx8G -XX:MaxMetaspaceSize=4G` — more than this machine has. Lowered to
768m/512m and, more importantly, cut the number of JVMs: Kotlin compiles
in-process, no persistent daemon, no parallel execution, one worker.
Also repairs a regression an earlier prettier pass introduced. That pass
pretty-printed test/fixtures/sample_receipt.json, but
`accepts bare JPK payload and bare JWS token` reads the fixture as raw TEXT
with a regex matching `"data":"..."` with no space after the colon. The
inserted space made the match null and the null-check operator threw.
Comparing the parsed JSON for equality had passed and proved nothing about
that raw-text contract.
The fixture is restored to its original minified bytes — a real e-paragon
arrives as single-line JSON — and test/fixtures/ is now in .prettierignore so
captured payloads stay byte-faithful. The regex also tolerates whitespace now
and asserts the match is non-null with a reason, so the same change would fail
readably instead of crashing. Verified against both the minified and the
pretty-printed fixture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A completely absent `prettier` binary went unnoticed long enough for
unformatted markdown to reach main. Two independent causes, both fixed here.
No pre-push hook existed. The three hooks declaring `stages: [pre-push]` in
.pre-commit-config.yaml — prettier, ci-mirror, pytest-coverage — therefore
never ran locally at all. Adds one.
The pre-commit hook failed open. shfmt and shellcheck were wrapped in
`if command -v`, and the entire pre-commit framework was skipped with a
warning when its binary was missing. A silently skipped gate is
indistinguishable from a passing one, so a missing tool now aborts.
- lib/common.sh: require_tool tries `sudo -n pacman -S` and aborts with the
exact command when a tool cannot be installed non-interactively;
verify_toolchain applies it to all eight binaries the hooks invoke.
- The pre-push hook resolves git's stdin refs into --from-ref/--to-ref.
Without a range, `pre-commit run --hook-stage pre-push` checks *staged*
files, and nothing is staged at push time, so prettier would have received
zero files and passed vacuously.
- ensure_hooks_path repairs core.hooksPath drift on every hook run.
- meta/scripts/install_hooks.sh covers the one case the hooks cannot
self-heal — a fresh clone where the config was never set, so nothing runs.
`--check` reports state and exits non-zero when incomplete.
Note `pre-commit install` must never be used here: it writes into
.git/hooks/, which git ignores entirely while core.hooksPath is set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On 2026-07-30 a kernel upgrade left the machine in systemd emergency mode
on three consecutive boots. The upgrade replaced /usr/lib/modules/<old>
with <new>, but the new kernel never reached the ESP, so the firmware kept
booting the OLD vmlinuz whose module tree had just been deleted. Nothing
modular could load after pivot-root — including vfat, which meant /boot
itself could not be mounted, local-fs.target failed, and systemd dropped to
an emergency shell. pacman reported the transaction as successful.
Root cause: pacman-git with ParallelDownloads > 1 segfaults every forked
alpm hook child, silently skipping 90-mkinitcpio-install and depmod.
boot-repair is self-contained (no repo libs — it must run in the emergency
shell) and fully offline (missing modules are recovered from
/var/cache/pacman/pkg). It repairs by default; --dry-run inspects.
It detects and fixes: read-only root, noauto/nofail fstab drift, orphaned
kernel files shadowing an unmounted ESP, unloadable vfat, unmounted ESP,
missing modules.dep, ESP kernel != installed tree, stale initramfs, and
ParallelDownloads > 1.
Two pacman hooks make the failure impossible rather than merely
recoverable: 05-boot-mounted-guard aborts a kernel transaction when the
ESP is unmounted (AbortOnFail), and 99-boot-autorepair fixes ESP drift
after the transaction, before the machine is ever rebooted.
Safety: a module tree counts as usable only if it has BOTH vmlinuz and
kernel/, so the metadata-only wreckage this bug leaves — which sorts
newer — is never chosen as the target. Files under /boot are deleted only
while it is not a mountpoint AND contains no EFI/ or loader/; deleting
after mounting would destroy the real kernel.
Verified:
- 31/31 fixture tests (--root against throwaway trees, no privileges)
- 11/11 reversible live tests on the real machine: unmounted the real ESP,
planted shadow files, confirmed they were deleted and the ESP then
mounted with EFI/, loader/ and kernel 7.1.5-arch1-2 intact
- Gate proven against a real transaction with the ESP unmounted:
"failed to commit transaction", no packages upgraded, nothing written
- shellcheck + shfmt clean, pre-commit passing
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate caught this: test_session_autopsy_hooks reads the DEPLOYED hook under
~/.claude, so repointing that hook to kuhyx/mcp-servers broke an assertion still
expecting `python_pkg.session_autopsy`. Exactly what a deployment-path test is
for — a test against a stale in-repo copy would have stayed green.
The replacement is stronger than what it replaces: it asserts both the new module
path AND that "python_pkg" is absent, so regressing to the old location fails
rather than passing quietly. The mutation checker confirms all 12 tests are still
killed by at least one mutation.
Also adds the evidence/contract artifacts for the extraction. They were written
for 627b9e7 but never committed: that commit was pure deletions, and the
ai-evidence-contract hook does not treat deletions as code changes, so it passed
without them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The solver only ran behind ./run.sh, had one media query in the whole
stylesheet, and kept the inventory in a 46vh scroll box that showed maybe a
third of the 43 dice. Setting it up on the PC did nothing for the phone.
Deployment
dice.kuhy.duckdns.org, behind the existing gitea-caddy edge. DuckDNS already
wildcards *.kuhy.duckdns.org (checked against 1.1.1.1 and 8.8.8.8), so no
dashboard step. deploy_build.sh builds to dist.staging and rsyncs into dist/
with --delete-after --delay-updates; plain --delete removes stale assets
before writing the new ones, which 404s a client mid-load. A post-commit hook
starts a MemorySwapMax=0 systemd user unit, so a rebuild can neither slow a
commit nor thrash zram.
Three traps found by driving the live URL in a browser rather than curling it:
a site address of "127.0.0.1:8089" reads as a HOSTNAME, so Caddy enabled
auto-HTTPS and started an HTTP->HTTPS redirect listener that bound *:80 next
to the edge -- SO_REUSEPORT would then split inbound HTTP and intermittently
break ACME for every domain here. The same address also made a Host matcher,
so the edge's forwarded Host matched nothing and served an empty 200 while the
local health check passed. And watching dist/'s own inode could never detect a
detached bind mount: rsync into an existing directory never changes it. Each
now has a guard that asserts positively rather than failing open.
Dice list
All 43 on screen at every width, no inner scrollbar. Column count and size
tier are chosen together from a container query on the panel -- not the
viewport, which mispredicts by 2x at the point the columns split. Two
invariants govern it: the list fits above the fold, and the name column stays
wide enough to tell Painter's die B, G and R apart. The left column holds the
grid alone, so it is sized to fill the height it owns: one --die-row value
drives the type, buttons and bar strip, giving a 52px row and 44px buttons on
a 1296px-tall screen and 20px rows on a phone.
Three safe-design rules are broken inside .die-list deliberately: sub-16px
body text, square icon buttons, sub-8px intra-cell gaps. 43 items with a name,
a chart and a counter in one viewport is arithmetically incompatible with 16px.
Import / export
A file, the clipboard, or a link carrying the inventory in the URL fragment.
One validator behind all four entry points including localStorage; it keeps
what it can and reports what it dropped, so a die added in a later game patch
costs you that entry, not the inventory. A link never silently overwrites what
is saved here -- it offers Keep or Discard, and only a link that actually
carries dice is applied at all, because one encoding a count of zero parsed
cleanly, blanked the screen, disabled Clear and left Keep writing the emptiness
over the visitor's own loadout.
session_autopsy
parse.py (551) and report.py (504) were over the repo's 500-line limit, so
test_file_length has been failing on main since 5ffd035. Extracted TurnTracker
and the two candidate-cell helpers. Report output is byte-identical on real
data apart from its timestamp, checked against HEAD in a worktree.
Verified: 288 TS tests and 911 Python tests, 100% coverage both, lint and
pre-commit clean, 8 viewports measured on the live HTTPS site, solver confirmed
through the real module worker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`git commit`/`git push` export GIT_DIR to every hook they run, and the
flutter tool shells out to git to identify its own SDK. `git -C
"$FLUTTER_ROOT" rev-parse HEAD` in bin/internal/shared.sh does not beat an
inherited GIT_DIR -- `-C` only changes directory, GIT_DIR overrides
repository discovery outright:
$ git -C /opt/flutter rev-parse HEAD
058e0af2c2... <- Flutter's HEAD
$ GIT_DIR=~/diet-guard/.git git -C /opt/flutter rev-parse HEAD
84bd550331... <- diet-guard's HEAD
So inside a hook, flutter reports the project as the SDK:
Flutter 3.47.0-0.1.pre • channel main • https://github.com/kuhyx/diet-guard
pub then resolves the SDK as 0.0.0-unknown and every version constraint
becomes unsatisfiable. flutter also writes that into its own version cache,
so the wrong answer outlives the hook and follows the next caller into an
unrelated repository -- which is why the identical command passes when you
re-run it by hand and fails again on the next push.
diet-guard's pre-push gate hit this today. Clearing the variables per call in
each hook works, and screen-locker and diet-guard now do, but only for as
long as every future hook author remembers -- deployment hygiene, not a fix.
This installs a wrapper ahead of /usr/bin on PATH that strips them for every
caller in every repo, covering both flutter and dart. It removes its own
directory from PATH before exec so the Flutter entry point's internal `which
flutter` cannot loop back into it, and it verifies itself by running flutter
against a throwaway poisoned GIT_DIR and failing unless the real SDK comes
back.
Not patching bin/internal/shared.sh: that file belongs to the flutter-bin
package, so the next update would silently restore the bug. Upstream already
unsets these variables in content_aware_hash.sh and update_engine_version.sh,
just not in the version path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The i3 `exec` line pointed at
/home/kuchy/linux-configuration/scripts/utils/turn_off_auto_idle_screen_shutdown.sh
- a typo'd home ("kuchy") *and* a path that stopped existing when the repo
was reorganised in db6276b. The exec has been failing silently at every
login ever since, so the idle inhibitor was simply not running: a controller
was connected with no "idle-off" lock held, meaning the screen could blank
and lock during controller gaming, which is exactly what the line exists to
prevent.
Point it at the script's real location.
Also make --watch-controller a singleton. It is a long-lived watcher started
from an i3 `exec`, and i3 re-runs those on every `i3-msg restart`, so each
restart would leave another watcher holding a redundant inhibitor for the
rest of the session. One flock, held for the life of the watcher.
Verified: the watcher now holds
idle-off 1000 kuhy <pid> systemd-inhibit idle "game controller connected"
and a second invocation declines instead of stacking a second inhibitor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ⚙ statusline badge counted all 182 candidates — meaningless. It now
counts candidates ≥100k est weekly tokens (7 today): a number small enough
to mean 'worth your time'. token-savers.zsh gives the compiled scripts
shell entry points (finish-gate / phone-deploy / autopsy / yay-clean) so
the deterministic halves cost zero LLM tokens. Adds the first explanation
doc produced by the newly compiled explain-diff scaffold.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a83b5ab took the analyzer package to 100% branch coverage but stopped at the
package boundary. Its only production entry points are two shell hooks in
~/.claude/hooks, and those were untracked, unlinted and untested -- the same
"untracked meant unlinted" shape that commit's own message describes, one
directory over. Both run on every session on this machine.
Both hooks end in an unconditional exit 0 by design: an autopsy failure must
never surface into the session lifecycle. That makes exit status a constant, so
a test asserting it passes against a hook whose body has been deleted. Every
assertion here is on a side effect instead. For SessionEnd a setsid shim on PATH
records the launch argv rather than spawning an ingest, so the tests can pin the
command, transcript path, --quiet, PYTHONPATH and timeout on the happy path, and
pin no-launch-at-all for a missing key, an empty path, a nonexistent file and
malformed JSON.
SessionStart asserts stderr as well as stdout. Under set -u, (( count > 0 )) on
a non-numeric count raises an arithmetic error to stderr while stdout stays
silent, so a stdout-only assertion would miss a hook injecting bash noise into
every session start. That gap was found by mutation, not by inspection.
A pytest file rather than a test_*.sh, deliberately: only .py under
linux_configuration/tests is collected by pytest_changed_packages.py, so this
lands in the local pre-commit gate, whereas the .sh tests there are gated by the
shell-tests workflow alone. It imports nothing from python_pkg, so the
100%-on-python_pkg gate is untouched -- 977 pass at 100% branch.
Verified by a per-guard mutation matrix rather than one whole-body gutting:
gutting alone kills only the three positive tests and would have falsely branded
the nine negative ones vacuous. Dropping each guard in turn, widening > 0 to
>= 0, and announcing unconditionally kills all twelve. The matrix also showed
that the start hook's [[ ! -r "$STATE" ]] guard is redundant -- deleting it kills
nothing, since jq's stderr is suppressed and an empty count already fails the
arithmetic. Left in place as defence in depth; the hooks are unmodified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Caps CPU (amd-pstate-epp) and GPU (nvidia-smi power limit) during the
existing night-lockdown window instead of migrating services to a
Raspberry Pi (tried, abandoned after SSH auth failed). A more aggressive
GPU PCI-unbind option is built but left disabled by default.
Also fixes an unrelated live incident found while testing: dnsmasq (this
PC's LAN DHCP/DNS server) had died with zero error logged, breaking WiFi
for every device on the network. Added a watchdog timer mirroring the
existing shutdown-timer-monitor-watchdog pattern, and whitelisted the
Tapo smart plug app so the overnight power reading is checkable during
the phone's curfew window.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The unlock script started morning-routine.service on every staggered trigger
(05:00/05:15/05:30/06:00/07:00) on alarm days. That was a workaround for the
always-on lockdown removing the hibernate/resume event the alarm used to ride
on — reasonable when nothing else could fire it.
Something else fires it now. wake-alarm-trigger.timer ticks every minute and
starts morning-routine.service at the time the phone synced, with a catch-up
window for a suspended or just-booted machine. Keeping both meant two
independent triggers, one of which ignores the phone entirely.
That is not hypothetical: on 2026-07-20 the phone was set to 08:00 and the
alarm went off at 07:00, started here, with no wake-alarm-trigger "Firing" line
in the journal at all. The 08:00 fire then landed as a no-op because this
07:00 run had left morning-routine.service still active.
Drop maybe_start_morning_alarm() and both call sites, plus the config that
went dead with it (ALARM_DAYS_DOW, MORNING_ROUTINE_UNIT, the alarm_started_date
marker). MIN_WAKE_AFTER_LOCKDOWN_HOURS stays — the wake-floor trigger uses it.
The lockdown's actual job — console/GUI restore, unmute, unmasking the login
surface — is untouched.
The only starters of morning-routine.service left are wake-alarm-trigger.timer
(primary) and the system-sleep hook (resume backstop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test 10 only knew the retired shutdown-schedule-guard.path, so it
reported a skip while guard-file@shutdown-schedule.path was actively
protecting /etc/shutdown-schedule.conf. That understated the real
protection in the same way tests 2, 3 and 5 did before the last
commit.
It now accepts the guard-lib unit and passes. The
disabled-is-intentional branch stays for hosts running neither unit.
Host: 19 passed / 0 failed / 5 skipped. Container: exit 0.
The suite reported four failures on this host and none of them meant
protection was missing.
Tests 2, 3 and 5 checked hosts-guard.path, hosts-bind-mount.service
and nsswitch-guard.path. The 2026-07-17 guard-lib refactor replaced
those with templated units — guard-file@hosts.path,
guard-bind-mount@hosts.service, guard-file@nsswitch.path — which are
all active. That is also why the legacy units stop a second after
boot: they are superseded, and the old nsswitch guard retriggered
itself into systemd's start limit fighting the new watcher. Each check
now accepts either generation, and the bind-mount check also accepts
the read-only mount on /etc/hosts, which is the real invariant.
Test 10 now treats a *disabled* shutdown-schedule-guard as intended
rather than broken: the poweroff flow it protected was replaced by
night-lockdown-enter.sh. An enabled-but-inactive unit still fails.
That check also had a shell bug — `systemctl is-enabled` prints the
state and exits non-zero when disabled, so `|| echo disabled`
appended a second line and never matched.
Verified protection is genuinely intact rather than assuming it:
/etc/hosts is mounted ro,noatime and both it and /etc/nsswitch.conf
carry the immutable flag. Host now reports 18/0/6. A container with
faked host markers still reports 10 failures, so the suite has not
been softened into always-green.
No system state was changed.
The last three exclusions are gone.
test_i3blocks_efficiency.sh only ever needed strace and ptrace. It went
unverified earlier because Docker's bridge network has no DNS on this
machine, so pacman could not install strace; --network=host works, and
the test passes. The job installs strace and adds SYS_PTRACE.
test_hosts_guard_pacman_integration.sh asserted three symbols the
guard-lib refactor renamed (pacman_hooks_manage_hosts_guard ->
pacman_hooks_manage_guard_lib, and similar). The feature was intact;
only the assertions were stale.
test_security_hardening.sh now separates live-host checks from
repository checks. Off-host the live ones skip, so it is green in a
container while still running its repository checks — and on the real
host it still fails loudly when a guard unit is down.
That last part needed care: the first detection attempt used
`systemctl is-system-running`, which exits non-zero on a degraded
system — precisely the state a failed guard unit produces. It masked
all four real failures. Detection now tests for /run/systemd/system
plus the installed guard payload, which unit health does not affect.
Verified: all 8 arch tests pass in a container; the same
security-hardening file still reports 4 failures on this host.
Adds an arch-tests job on archlinux:base-devel so the pacman wrapper,
makepkg wrapper and hosts-guard tests finally run somewhere. They need
an Arch userland and root, which a disposable container provides.
Four more tests still referenced pre-reorganisation paths and could
never find their targets; repairing them took the container set from
two passing to five. All five were verified with --network=none, so
the job installs nothing beyond git (base-devel ships no git, and
actions/checkout needs it).
Also removes the silent-failure mode behind the earlier
music_parallelism confusion. The test now derives its temp-worktree
path from the script's real position instead of hardcoding the depth,
checks up front that ../../lib/common.sh resolves to the stub, and has
the stub write a marker asserted after every run. Previously a
reorganisation made the script source the real
/usr/local/lib/common.sh, so the test measured the live system and
failed as though the daemon were broken.
Three tests stay out, each for a different reason, documented in the
workflow: test_i3blocks_efficiency.sh needs strace and could not be
verified locally (containers here have no DNS);
test_hosts_guard_pacman_integration.sh fails a real assertion worth
investigating; test_security_hardening.sh asserts live host state and
can never pass in a fresh container.
Both tests excluded from CI last commit turned out to be stale rather
than catching regressions. Neither covered script needed changing.
test_music_parallelism.sh built its temp worktree at
scripts/digital_wellbeing/, but music_parallelism.sh resolves its
library as $SCRIPT_DIR/../../lib/common.sh — two levels up, correct
for the real scripts/periodic_background/digital_wellbeing/ location.
From the shallower path that resolved outside the worktree, so the
stubbed common.sh was never sourced and the script fell through to the
real /usr/local/lib/common.sh on the machine. The test was exercising
the live library and ignoring its own MOCK_FOCUS_ACTIVE, which is why
idle mode looked like it waited 15s instead of 30s. The daemon was
correct all along.
test_usage_monitoring_installer_efficiency.sh demanded a literal
'sleep 60'. The logger template no longer polls: it sleeps once until
midnight via seconds_until_next_day, then blocks on wait for the pmon
child — strictly fewer wakeups than the assertion required. Replaced
with assertions on that property so a return to polling still fails.
Allowlist extended from four tests to six.
The shell tests under linux_configuration/tests/ never ran anywhere:
no workflow executed them, so they rotted silently. Three referenced
script paths that moved under scripts/periodic_background/ in an
earlier reorganisation, and test_android_guardian_service.sh created
its temp worktree at a different path from the one it copied into, so
its cp could never succeed.
The new workflow runs the bats suites plus an explicit allowlist of
side-effect-free tests. Roughly eight others drive pacman, /etc,
systemd or adb and need root plus an Arch container, so they stay out.
Repairing the paths uncovered two genuine failures, deliberately left
out of the allowlist because they look like real regressions in the
scripts they cover rather than stale assertions:
- test_music_parallelism.sh expects idle mode to wait
IDLE_CHECK_INTERVAL (30s); the daemon waits 15s, so it wakes twice
as often as intended when idle.
- test_usage_monitoring_installer_efficiency.sh expects the logger
template to sleep between day-rollover checks.
Both need a fix in the covered scripts, not in the tests.
Diagnosing "Steam downloads are slow" kept pointing at the ISP because
every averaged measurement showed a steady ~170 Mbps. Sampling at 1s
instead of 5s showed that plateau does not exist: the link alternates
between ~450 Mbps bursts and multi-second stalls. Effective rate is
burst x duty_cycle, and only the second factor was wrong.
steam-download-duty.sh reports those two factors separately, which is
what the Steam UI cannot do. line-speed-probe.sh separates burst from
sustained throughput, because a single stream tops out near 180 Mbps on
this line and reads convincingly as ISP shaping when it is not — a
many-connection swarm reaches 986 Mbps with zero retransmits.
Arithmetic is extracted into pure helpers with main() behind a
BASH_SOURCE guard so the bats suite can source them without starting a
measurement. install.sh installs bats and links both scripts onto PATH.
Note: no CI workflow runs .sh/.bats tests, including the four shell
tests that predate this one, so the suite is manual-only for now.
Steam/browser mutual-exclusion daemon was unusable in its current state.
Removed all code, install tooling, and every reference across
install_core_system.sh, check_and_enable_services.sh, the security
hardening test suite, and CLAUDE.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diagnosed the "claude keeps crashing + mouse-command garble" as three stacked
bugs (remoteControlAtStartup end_session collision, SIGTTIN suspend on early
input, and an upstream REPL-exits-on-startup-input). This carries the shell-side
mitigations and the tooling that caught them:
- _claude_mouse_heal precmd hook: disables mouse tracking before every prompt,
so garble left by a *suspended* claude clears on the next Enter (the on-exit
reset in claude() can't fire while the function is blocked in a suspend).
- Always-on --debug-file logging in claude() (~/.claude/logs, keeps last 20;
opt out with CLAUDE_DEBUG_LOG_OFF=1) -- this captured all three root causes.
- On-demand launchers claude-rag/-travel/-sports/-media/-extras load the niche
MCP servers via --mcp-config, keeping them out of the always-on global fleet.
- claude-debug raw-binary probe (note: zsh printf has no %(...)T directive).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZ15AWWYN8xGKdwXKn7foM
The alarm floor (lockdown-entry + 8h) from the previous commit still relied
on the fixed 05:00/05:15/05:30/06:00/07:00 unlock triggers to ever re-check
it. Shutdown time is configurable (/etc/shutdown-schedule.conf, changed by
the screen_locker sick-day feature) and can vary night to night, so a late
enough lockdown (e.g. 23:30) pushes the floor past every remaining trigger
(07:30 > 07:00) and the alarm would silently never fire that morning.
Enter script now schedules a transient systemd-run --on-calendar one-shot
timer at exactly lockdown-entry + MIN_WAKE_AFTER_LOCKDOWN_HOURS, computed
from the real lockdown timestamp rather than assumed shutdown hours. It
re-runs the idempotent, self-dedup'd unlock script, guaranteeing the alarm
gets a correctly-timed chance regardless of what time lockdown started.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4YWZhDcEUMUi8cyw9AzTR
The 23:00 lockdown -> 05:00 unlock cycle exposed three live bugs: a visible
console-text flash before the screen went black (blank_console only ran once
systemctl stop lightdm.service fully returned, leaving a gap), DP-0 needing a
manual xrandr/replug after wake, and the morning alarm firing only 6h after a
late lockdown entry instead of waiting a full 8h.
- Blank the console continuously in a background poller for the whole GUI
teardown instead of a single after-the-fact call.
- Retry autorandr --change against a saved profile after lightdm restarts.
- Stamp lockdown-entry time and gate the alarm on entry + 8h, decoupled from
the console/GUI-restore idempotency so it can fire on a later staggered
trigger instead of only the first one.
- Add DRY_RUN NOTE banners so a clean dry run is never mistaken for proof the
console-blank and autorandr fixes work for real (they need a live cycle).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4YWZhDcEUMUi8cyw9AzTR
Typing during Claude Code's ~5s startup triggered a SIGTTIN job-control
stop ("suspended (tty input)") that left mouse tracking / terminal probes
enabled, spamming SGR mouse (35;44;41M) and DSR (997;1n) sequences.
Upstream bug, closed "not planned" (anthropics/claude-code #7807#11898#23581#50032); no flag/env disables the probes.
Wrapper runs claude with job-control monitoring off (nomonitor) so it
can't take SIGTTIN, and resets mouse/focus/paste/cursor modes to /dev/tty
on exit as a self-heal. Opt-out via CLAUDE_TTY_FIX_OFF=1. Auto-loaded via
an oh-my-zsh custom symlink (same pattern as calc-live.zsh); .zshrc
untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fwBQdNPJPgKR3gcDZLRjo
focus_mode_daemon.py has been 509 lines since 22e22c1, over the repo's
cap. It went unnoticed because the Python tests workflow only triggers on
python_pkg/** changes, so that commit never ran CI; the next commit to
touch python_pkg/ exposed it and turned main red.
Extract configuration, logging, notification, /proc process inspection and
the killers into _focus_mode_lib.py, leaving the daemon its policy: the
whitelist, the FocusMode state machine, and main(). log/notify move with
the process code that calls them, so the dependency runs one way and there
is no cycle. The helper is underscore-prefixed and registered in pylint's
init-hook path, mirroring the existing usage_report.py / _usage_report_*.py
sibling-import precedent.
The daemon runs from a symlink in /usr/local/bin, so a sibling import only
works if Python resolves that symlink before setting sys.path[0]; verified
that it does, and the installer symlinks rather than copies, so the sibling
travels with it. Confirmed against the running service: restarts clean and
resumes enforcing, process detection unchanged, status output matches the
pre-split version.
Structural only; no behaviour change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RG3eEn8rwZbcXaJrKJxdv8
install_focus_mode_daemon.sh copied the script to /usr/local/bin, so the
deployed daemon went stale the instant the repo file was edited: what you
run stops matching what you read, with nothing to tell you. That is not
hypothetical - the zombie-detection fix in 22e22c1 sat in the repo while
a months-old copy at /usr/local/bin/focus-mode-daemon went on entering
GAMING mode and killing browsers. It only surfaced because the deployed
copy was checked by hand.
Link instead of copy, so editing the repo file IS deploying it. Also
chmod +x the source, since a symlink takes its executability from the
target rather than from an install mode.
uninstall now guards with [[ -e || -L ]] rather than [[ -f ]]: both -f
and -e follow the link and report false when the target is gone, which
is exactly the case (repo moved or deleted) where a stale link most
needs clearing.
The link resolves into the user's checkout, so /usr/local/bin/... is only
as trustworthy as that path. Fine here - focus-mode runs as a systemd
*user* service, crossing no privilege boundary. Not a pattern to copy for
anything root runs.
Verified: stat -L reports the same inode (92274745) for the deployed path
and the repo file; appending a marker to the repo file makes it appear at
/usr/local/bin/focus-mode-daemon immediately, with no reinstall; a link
to a missing target is now removed by uninstall where [[ -f ]] skipped it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rN2usTS3pYLJqeVg2ZpgR
Focus mode flapped in and out of GAMING mode every few seconds and killed
browsers while no Steam was running. The pattern matching was not at
fault: the daemon was correctly seeing a process named "steam". That
process was a root-owned zombie.
A total gaming block on 2026-07-04 purged the Steam client and it was
never reinstalled, but steam-backlog-enforcer kept trying to launch it
through an orphaned /usr/local/bin/steam wrapper still pointing at the
deleted /usr/bin/steam. Each launch died on exec and, never being reaped,
lingered as a dead process that still carried the name "steam" - which
this daemon counted as a live Steam.
get_running_processes() now reads /proc/*/stat rather than /proc/*/comm
and skips state Z. Both cost one read per PID, so the poll loop stays
fork-free, but stat also carries the process state. _parse_stat() uses
rpartition(')') because a process name can itself contain spaces and
')', which naive whitespace splitting would corrupt. Names are truncated
to 15 chars in stat exactly as in comm, so the existing patterns match
unchanged.
Verified against the live zombie, before the enforcer was fixed:
zombies on system now : ['steam']
OLD (/proc/*/comm) sees 'steam' : True
NEW (zombie-filtered) sees 'steam' : False
dropped by filter : ['steam']
and non-regression on real processes: a synthetic live 'steam' and a
'steam_app_29687' game both still return True. It skips zombies, not
Steam. Running `focus-mode-daemon` now reports "Browser detected -
entering BROWSING mode" with no GAMING flapping.
The zombie source is fixed separately in steam-backlog-enforcer d54df3a;
this is the durable guard against any future one.
Note: install_focus_mode_daemon.sh copies rather than symlinks, so
/usr/local/bin/focus-mode-daemon was a stale copy without this fix.
Reinstalled. Editing this file alone never changes what runs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rN2usTS3pYLJqeVg2ZpgR
The digital-wellbeing curfew powered the PC off at 21:00, which also took
down every service this machine hosts (Gitea, the Caddy TLS edge, SyncYomi,
the personal site, Open WebUI, Joplin, dufs, ollama, dnsmasq, wg-quick,
sshd). Keep the curfew's lockout, drop the power-off: the machine now stays
on with the GUI torn down, the TTY login surface masked, the console blanked
and the RGB killed, and restores itself at 05:00.
The fortress still owns the schedule (immutable config, ratchet, monitor,
watchdog); only its terminal action is swapped for night-lockdown-enter.sh.
The reversal deliberately lives outside the fortress so a bug in the lock
path can never take the recovery path down with it.
Verified in production overnight: lightdm down 21:00:04 -> 05:00:00 while
all 7 containers stayed "Up 14 hours" and kept serving; ticks idempotent all
night; 05:00 unlock fired.
Notable fixes found along the way:
- Wake alarm regression: it only ever fired on resume-from-hibernate, so an
always-on machine silently killed it. The unlock now re-fires
morning-routine.service on alarm days (the Mon/Fri/Sat/Sun gate lived in
the removed rtcwake branch, not in the alarm).
- Morning boundary -le -> -lt, so 05:00 no longer races the unlock.
- Mask getty@ only; autovt@ is an alias of it and masking it errors.
- Persist LOCKED before the session-killing teardown, else a kill mid-lock
leaves state=UNLOCKED and the unlock no-ops, stranding the machine.
- Blank the console without DPMS: FB_BLANK_POWERDOWN drops the DisplayPort
link and the GPU never re-trains it (DP-0 needed a physical re-plug).
- RGB: openrgb-git 0.9.2148's CLI returns before detection finishes, so every
call silently no-op'd while exiting 0. Stable openrgb 1.0rc3 fixes it. Use
static+black, not "off" (the ZOTAC has no Off mode; the ASRock ignores it).
- Validate the generated config with bash -n: its heredoc must stay unquoted,
so a stray backtick executes and corrupts the file.
Thu-Sun 22->21 aligns the source with the canonical schedule screen_locker's
sick-day feature ratcheted in on 2026-07-03; the ratchet only permits
same/stricter, so the script was otherwise un-re-runnable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dpu8SqqBVm97tCuTGE3nCs
Three of the four remaining items were scripts hardcoding assumptions that
stopped being true — not real drift.
hosts (phantom warning): check_hosts looked for 10-unlock-etc-hosts.hook /
90-relock-etc-hosts.hook, gone since the guard migrated to guard-lib's generic
unlock-all/relock-all hooks. Match what pacman_wrapper.sh already treats as
authoritative (legacy pair still accepted).
focus_mode (never installable): install_focus_mode_daemon.sh calls
`systemctl --user`, but the checker invokes it via `sudo -u <user>` from root —
sudo changes the UID but grants no session, so XDG_RUNTIME_DIR /
DBUS_SESSION_BUS_ADDRESS were unset and it died at daemon-reload on every
automated run. Derive them from the effective UID. Now enabled+active.
compulsive_blocker (discord never limited): REAL_BINARIES hardcoded
/opt/discord/Discord — another discord package's layout. This one ships only
/usr/bin/discord, a bootstrap that downloads the app into ~/.config/discord/
and execs ~/.config/discord/Discord (a version symlink its updater maintains),
so the `[[ -x $real_binary ]]` gate failed forever. Exec the preserved LAUNCHER
(/usr/bin/discord.orig) so Discord's bootstrap/self-update still runs. Two
supporting fixes: the gate skips -x when the exec target IS the wrapper's own
.orig (the wrap creates it — chicken-and-egg), and PROCESS_MATCH is added
because the launcher exec()s away, so pgrep -f on '.orig' matches nothing and
auto-close/kill/running-state would have silently no-opped.
vbox_hosts: skipped -> n/a with a reason. VirtualBox is absent AND blocked by
pacman policy ("can bypass /etc/hosts restrictions"), so the check is not
applicable, not ducked. Installing it to satisfy a hosts check would open the
hole that check exists to close.
Also removed thesis_tracker (unused; unit not-found, script never installed).
Verified: every service ok, vbox_hosts n/a, zero errors, zero warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
check_and_enable_services.sh was lying about two things and tracking a third
the author no longer uses.
1. workout_locker: paths still pointed at python_pkg/screen_locker/, which
stopped existing when screen-locker was extracted to its own repo
(~/screen-locker). The service was installed, enabled and active the whole
time, but the checker reported a red error and its "fix" silently did
nothing. Now resolves ~/screen-locker via SUDO_USER + getent (the script
re-execs under sudo, so $HOME is /root).
2. User-service queries could never work: `sudo -u <user> systemctl --user`
has no XDG_RUNTIME_DIR/DBUS_SESSION_BUS_ADDRESS, so it always failed with
"Failed to connect to user scope bus" and reported EVERY user service as
not-enabled. Added user_systemctl() using
`systemctl --user --machine=<user>@.host` (systemd's own suggested fix) and
rerouted all 6 call sites (focus-mode, thorium, workout-locker).
3. Removed thesis_tracker entirely — not used; unit not-found, script never
installed. Help list renumbered.
focus_mode and guest_mode_removal keep reporting error: those are GENUINE
(focus-mode.service is not-found; the guest-mode policies are absent).
Verified: ground truth (as kuhy) workout-locker.service = enabled+active.
Before: checker said "not enabled". After: workout_locker ok, all 3 checks ✓.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
setup_hosts_guard.sh died partway on every run under `set -euo pipefail`,
which is why hosts-guard.path / hosts-bind-mount.service / nsswitch-guard.path
sat installed-but-disabled since 2026-02-02 and resolved-guard.path was never
installed at all — /etc/hosts had no watcher to restore it if tampered.
Two aborts, both silent:
1. `local_dot=$(grep -E '^\s*DNSOverTLS\s*=' ... | tail -1 | sed ...)` — grep
exits 1 when the key is absent, pipefail propagates, the assignment returns
1, set -e kills the script. DNSOverTLS is absent from a default
resolved.conf, so this fired on EVERY run, right before resolved-guard.path
was enabled. The fix-if-missing branch below it was unreachable because
detecting "missing" aborted first. Same trap on the ReadEtcHosts probe.
check_and_enable_services.sh already guarded its copy with `|| true`.
2. `systemctl restart systemd-resolved` — resolved is MASKED here (dnsmasq
serves DNS), so the restart failed and aborted the script on its last step,
after everything was already configured. Now skipped with a note.
Before: exit 1, resolved-guard disabled, nsswitch-guard start-limit-hit.
After: exit 0; hosts-guard.path, hosts-bind-mount.service, nsswitch-guard.path
and resolved-guard.path all enabled+active. DNS still resolves; getent ahosts
on doubleclick/googlesyndication/facebook returns 0.0.0.0; /etc/hosts still
immutable at 176888 lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
Both tools swallowed failures. execute_with_log appended one line to a log
nobody reads and returned 0, so the systemd oneshot reported SUCCESS.
check_and_enable_services printed one err line among ~100 and exited 0.
That is why the 2026-05-15 reorg silently killed the pacman-wrapper and
hosts self-repair for five months with no visible symptom: check_hosts
detected the disabled hosts guards on every single run, could not find
setup_hosts_guard.sh at the stale $CONFIG_DIR/hosts path, and still exited 0.
Driver — any failure now surfaces four ways:
1. journal at ERROR priority (logger -p user.err)
2. the systemd unit FAILS (exit 1) -> systemctl --failed / red timer
3. a critical desktop notification that never auto-dismisses
4. /var/lib/periodic-system-maintenance/LAST_RUN_FAILED breadcrumb
It also collects ALL failures rather than aborting on the first.
check_and_enable_services — a missing repair script means this tool cannot
do its job, so all 12 "script not found" sites now route through
err_missing_script(): journal ERROR priority, a terminal banner, and exit 2.
Healthy runs still exit 0 and clear the breadcrumb.
Verified: driver fed nonexistent paths -> exit 1, all 3 failures on stderr,
breadcrumb written, 3 journal ERROR entries. Live regenerate + real run ->
rc=0, all steps ok, no breadcrumb. Checker --status -> rc=0, no banner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
Immutable git-tracked files break git tooling. The pre-push ci-mirror gate
runs `pre-commit run --all-files`; end-of-file-fixer opens every file rb+
and dies with "PermissionError: Operation not permitted", so no push from a
normal checkout could ever succeed — every push had to be routed through a
throwaway worktree (a fresh checkout gets new, non-immutable inodes). The
same immutability also breaks pre-commit's unstaged-changes stash: the
`git checkout -- .` unlink fails, the restore never runs, and unrelated
unstaged edits are silently reverted.
Enforcement does not depend on these sources being locked: /etc/hosts is
itself chattr +i, hosts-guard.path watches and re-enforces it via
enforce-hosts.sh against the locked-hosts canonical snapshot, and
hosts-bind-mount.service pins it. Editing the sources changes nothing until
install.sh is re-run as root, which regenerates the guarded artifacts.
Companion to 6e3d61d (same fix for setup_midnight_shutdown.sh).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
An immutable tracked file with uncommitted edits breaks every commit/push:
pre-commit clears unstaged changes with `git checkout -- .`, that unlink
fails ("Operation not permitted"), the context manager aborts, and the
restore never runs — silently reverting the user's OTHER unstaged edits
(seen twice today; an unrelated i3/config WIP had to be recovered from
pre-commit's rescue patch).
Enforcement is unaffected and does not depend on the source being locked:
/etc/shutdown-schedule.conf stays chattr +i, guard-lib keeps a canonical
snapshot + path watcher that re-enforces it, the monitor service protects
the timer, and screen_locker's ratchet only accepts same-or-stricter
values. Editing this source changes nothing until setup is re-run as root,
which rewrites the guarded config anyway.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
Vendored makepkg's run_pacman() waits on /var/lib/pacman/db.lck existence
with no timeout BEFORE invoking pacman, so an orphaned lock hangs
`makepkg -i` forever ("Pacman is currently in use, please wait...") and
the pacman wrapper's stale-lock cleanup never gets a turn. Wrap makepkg
(mirroring the pacman-wrapper pattern) to clear an orphaned lock up front.
- Extract stale-lock logic into shared pacman_lock_lib.sh; pacman_wrapper
sources it after integrity verification and it is in the SHA256 manifest
- makepkg_wrapper: pre-flight clear for -i/--install; bypass on FAKEROOTKEY
and non-install; FAIL OPEN if the lib is missing (never brick makepkg)
- Cross-user pacman_process_running() pgrep guard: unprivileged makepkg can
detect a root pacman its own fuser cannot see (avoids removing a live lock)
- Upgrade survival: rewrap hook (Install/Upgrade only) + check_makepkg_wrapper
drift verifier + both wrappers wired into the hourly periodic driver
- Fix reorg path bugs: stale ~/linux-configuration periodic path,
$CONFIG_DIR/hosts -> scripts/periodic_background/hosts, test WRAPPER_DIR;
refresh .orig only when target isn't already our symlink (no exec loop)
- Tests: test_makepkg_wrapper.sh (18) incl. hook Operation validity; add
lib-integrity coverage to test_pacman_wrapper_security.sh (also path-fixed)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
Serve the personal website (extracted to github.com/kuhyx/personal-website)
at the root kuhy.duckdns.org behind the shared gitea-caddy edge, moving Gitea
to gitea.kuhy.duckdns.org.
- setup_gitea.sh: relocate Gitea to gitea.kuhy.duckdns.org and refactor the
shared Caddyfile into per-service sites/*.caddy imports (fixes the latent
bug where re-running dropped the dufs/syncyomi blocks).
- setup_personal_website.sh (new): idempotent setup/status/help; migrates
existing Caddy blocks to snippets, relocates Gitea, builds ~/personal-website,
serves dist/ from a loopback caddy (admin off) on 127.0.0.1:8088 fronted on
the root domain, reuses the DuckDNS/firewall edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KG7yQj4hP8DDpasK2e4cn7
Stands up SyncYomi (ghcr.io/syncyomi/syncyomi:v1.1.11) in Docker behind
the existing shared gitea-caddy edge at kuhy-sync.duckdns.org, so
TachiyomiSY syncs its library from a mobile network. Follows the
setup_dufs_cloud.sh pattern: loopback-bound container, one appended Caddy
site block (auto Let's Encrypt), the shared DuckDNS updater extended, and
80/443 opened via setup_wireguard_ssh.sh. Auto-generates stored 0600
credentials, onboards the first account, and mints/reuses an API key over
the local REST API, printing host + key. Idempotent setup/status/help.
Verified live: container up (restart=unless-stopped), external HTTPS
reachable, and a real phone sync landed a 13.9 MB sync_data row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbvNNUYcdNtg5NjAhYc12E
Move cloud_gallery/, the Flutter dufs_client app, and the dufs/cloud setup
scripts into a new public repo (with full git history), leaving pointers here.
The live media-cloud-sync systemd units were re-pointed to ~/dufs-cloud/scripts/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
New reusable utility that extracts each media_archive_*.zip snapshot, MOVEs its
media into ~/cloud/Media/YYYY/MM via sync_media_to_cloud.sh (dedup across the
overlapping snapshots), verifies staging drained, then deletes the zip — only
after its media is confirmed in the cloud. Staging lives on the cloud
filesystem so moves are atomic renames.
Ran over ~/Downloads/media_archive_archive: 44 zips / 27 GB → 44 imported +
deleted, 0 kept. The two 13.7 GB near-duplicate snapshots collapsed (2412
duplicates dropped). Cloud Media 4.9 GB → 19 GB (2949 files); Downloads
reclaimed 27 GB; empty archive dir removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
Replace dufs's plain file list with a React 19 + Vite + TypeScript SPA served
same-origin by dufs, so the cloud URL becomes a real gallery behind the existing
auth: thumbnail grid, image lightbox (zoom + keyboard nav), inline video, drag/
pick upload, download, confirm-guarded delete, and a simple .txt/.md editor.
Serving architecture (de-risked against the real dufs 0.46.0): --assets does not
override the UI, so dufs `render-spa` serves the app shell, the SPA lists dirs
via WebDAV PROPFIND (render-flag-immune) and streams files via GET (Range for
video seeking), with HashRouter so navigation stays at "/". Verified render-spa
leaves KeePass file GET/PUT/?hash intact (Phase 1 unaffected).
Adds generate_thumbnails.sh (ImageMagick images + ffmpeg video posters →
~/cloud/.thumbs, idempotent) and setup_cloud_gallery.sh (build + deploy the SPA
into the cloud root, enable render-spa, wire thumbnail refresh into the Phase-2
media sync). setup_dufs_cloud.sh now emits render-spa only when the SPA is
deployed, so a fresh dufs keeps its default listing.
Verified: pnpm build + lint (strictTypeChecked) clean; 38 vitest tests, coverage
92.8% stmts / 96% lines (dufs-client 100%); local dufs --render-spa serves shell
+ assets + thumbnails + PROPFIND (all 200). Live deploy left for the user (needs
sudo; enables render-spa on the public service). Note: host ffmpeg is broken
(libvpx.so.11) so video posters are skipped until it's fixed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
Phase 2 of the cloud roadmap. sync_media_to_cloud.sh MOVES organized images and
videos out of ~/Downloads into ~/cloud/Media/YYYY/MM (by mtime), unzipped and
browsable via the dufs URL — no duplication, the cloud folder becomes the single
storage. Move is crash-safe (mv: atomic on same fs, copy-then-unlink across fs);
files modified in the last 2 min are skipped (in-progress downloads); identical
files are de-duplicated and name clashes get a numeric suffix.
setup_media_cloud_sync.sh installs a oneshot service + 30-min catch-up timer +
a ~/Downloads path watcher, plus a drop-in ordering the existing media-organizer
after the cloud sync so media is moved before it would be zipped.
Verified: shellcheck clean; 12-assertion MOVE suite on throwaway dirs; real
move of a PNG into ~/cloud/Media fetched via the live URL (HTTP 200 image/png).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
Add "one vault, synced everywhere" for PC + Ubuntu laptop + phone, with an
identical flow and no stored master password. keepass-open prompts the master
password (in memory only), folds in strays, pulls+merges the cloud copy over
WebDAV, opens KeePassXC on ~/Keepass/Passwords.kdbx, and publishes edits on
close. Safety rests on `keepassxc-cli merge` (entry-level union) plus a dufs
?hash compare-and-swap on upload, so concurrent edits merge instead of clobber.
The consolidator enforces exactly one vault per machine: it folds a stray only
if it opens with the same master password, backs up first, never removes the
canonical, and auto-excludes the dufs serve-path (the remote store) — a
data-safety fix after the first real run tried to delete ~/cloud/Keepass.
Only the dufs *server* password (not the vault key) is cached in the keyring.
Single cross-distro installer, no role flag; the phone uses KeePassDX natively.
Verified: shellcheck clean; 8 (reconcile) + 14 (consolidator) + 8 (installer)
assertions on throwaway vaults; real PC apply with live WebDAV GET=200 and
?hash==sha256; a clean keepass-open run ("no strays / already in sync").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
Add setup_dufs_cloud.sh: a self-sustaining installer that stands up dufs
(single Rust binary; web UI + built-in WebDAV) on 127.0.0.1:5000 behind the
existing Caddy + DuckDNS exposure stack at a new subdomain, so files are
reachable from mobile network over HTTPS. An event-driven systemd path unit
mirrors ~/Keepass/Passwords.kdbx into the served folder on every change, so
KeePassDX syncs the vault natively over WebDAV.
Paranoid secret handling for the gitignored .dufs_cloud.conf (plaintext web
password): ignored at repo-root + linux_configuration + **/ globs; a new
check-no-dufs-conf pre-commit hook blocks it even against `git add -f`; the
installer refuses to write it unless git check-ignore confirms it is ignored;
the password is hashed via `openssl passwd -6 -stdin` (never in argv); and a
cloud-root guard forbids serving home/.ssh/.config/.gnupg/the repo/root.
Verified: dufs auth (401/200) + WebDAV (207) against the real v0.46.0 binary,
idempotent Caddy/DuckDNS/unit-render logic, and a live phone-on-cellular test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
setup_local_llm.sh: idempotent Bash installer for a self-hosted local LLM
stack tuned for the RTX 3090 (24GB) — installs ollama-cuda, writes a
systemd drop-in enabling flash attention + Q8_0 KV-cache quantisation,
pulls the model, creates a custom Ollama model with a developer system
prompt, and launches Open WebUI via Docker.
Verification: bash -n and shellcheck -x clean (see evidence artifact).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
Add a self-expiring override system so specific late-night live events
(2026 World Cup matches) can be watched without the auto-shutdown killing
the PC, without weakening the permanent schedule ratchet:
- shutdown-override-manager.sh (add/list/remove): add is friction-gated
(typed confirmation phrase + 30s delay, max 12h, max 3 windows) and
writes absolute-epoch windows to /etc/shutdown-schedule-overrides.conf.
- day-specific-shutdown-check.sh now skips the shutdown decision inside an
active window (pruning expired entries) and gained a DRY_RUN guard around
rtcwake/poweroff/hibernate for safe testing.
i3blocks shutdown_countdown.sh: show the exact next-shutdown clock time
(not a relative countdown), show the overridden end time in green when an
override is active or will rescue the upcoming shutdown, and make the
skip-dates check fork-free (builtin read loop) so the hot path spawns no
external processes.
install.sh: deploy i3blocks by symlinking each repo file into
~/.config/i3blocks instead of cp -r (the source of silent drift), cd to
the script dir so it runs from anywhere, and chmod +x only *.sh so the
config file stays 0644.
Repair the previously-dead i3blocks regression suite (stale path), update
the shutdown assertion to the new absolute-time output, add hermetic
override coverage, and keep the <=1-exec fork-count guarantee.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmwYH5RfThZGkVD2jE61Da
setup_dns_blocker.sh turns this PC into a DNS server that serves the
identical /etc/hosts blocklist to the whole LAN with no per-device app,
by feeding generate_hosts_file.sh output into dnsmasq via addn-hosts
(same StevenBlack variant + custom entries + unblocks). Adds status/
refresh subcommands, a daily refresh timer, Restart=always, and logging
to /var/log/dns-blocker/.
Because the ISP router cannot advertise a custom DNS server, a dhcp/
dhcp-off mode makes the PC the LAN DHCP server (static IP via nmcli with
a gateway-ping self-check and safe, always-online rollback), handing out
itself as the DNS server so every device is blocked automatically.
setup_wireguard_ssh.sh gains an allow-dns subcommand that opens DNS (53,
LAN saddr) and DHCP (67) in the single-owner nftables ruleset. The DHCP
accept rule is placed before 'ct state invalid drop' because the
0.0.0.0->255.255.255.255 DHCP broadcast is classified INVALID and was
otherwise dropped. Firewall detection now reads the actual loaded ruleset
(a ruleset can be loaded while nftables.service reads inactive) and avoids
a grep -q/pipefail SIGPIPE false-negative.
Verified end-to-end: Pixel-6a leased 192.168.1.82 with DNS 192.168.1.43;
youtube.com blocked, legit sites resolve. Regression tests added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018pqSnrLt8QoWup7n2Ymk77
Replaces the bespoke chattr/bind-mount/systemd-watcher implementations for
/etc/hosts and /etc/shutdown-schedule.conf with the new shared guard-lib
(~/guard-lib, guardctl), so screen-locker and steam-backlog-enforcer's new
block-gaming feature stop maintaining parallel copies of the same
tamper-resistance mechanism.
- pacman_wrapper.sh: pre/post hook fallbacks now call guard-lib's generic
unlock-all/relock-all scripts (covers every registered guard instance,
not just /etc/hosts)
- setup_midnight_shutdown.sh: installs/updates its guarded config via
guardctl file-guard instead of hand-rolled chattr + systemd unit
generation; the schedule ratchet logic (block-if-more-lenient) stays
bespoke since guardctl's generic unlock can't represent it
- new hosts/guard/plugins/nsswitch-plugin.sh, resolved-plugin.sh
Also fixes, at user's request even though pre-existing: 3 shellcheck
SC2329 false positives in pacman_wrapper.sh (functions invoked indirectly
by name, not actually dead) and 1 SC2001 style warning (echo|sed VM-name
extraction replaced with parameter expansion, verified equivalent output).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFNiYQQgSLAkiBXswyimPq
Deploys Gitea+Caddy (auto-HTTPS via Let's Encrypt) at kuhy.duckdns.org,
extends setup_wireguard_ssh.sh with an allow-web firewall subcommand, and
mirrors all 21 GitHub repos (5 private) via Gitea's native pull-mirror.
Runs containers with host networking to work around a discovered bug where
this host's nftables forward-chain silently blocks Docker bridge egress.
Adds a self-hosted-service-exposure skill capturing the reusable pattern
and gotchas for future public-facing deployments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5jnu99ZuENSkuFQKLcSdh
The hibernate-on-alarm-night case block in create_shutdown_check_script()
escaped $ as \$ for an unquoted heredoc, but the heredoc is opened with the
quoted delimiter <<'EOF', so the backslashes were written literally into
the deployed check script. Every timer fire since commit 0d54c5d hit a bash
syntax error and exited before evaluating the shutdown window, so the PC
never auto-shut-down (confirmed failing at 23:00/23:01 last night and 07:01
this morning). Also fixed an off-by-one (seq ... 24 -> 23) that was emitting
invalid OnCalendar=24:00:00 entries systemd had to ignore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGHLfbAPmak6HtGJby57eo
Lets SSH terminal access reach this PC from a phone on a different network
(mobile data vs home broadband), using only FOSS/free software: self-hosted
WireGuard (no relay/coordination server), DuckDNS for the dynamic public IP,
and a default-drop nftables firewall so sshd is never exposed to the WAN
directly -- only the WireGuard UDP port is forwarded, SSH is reachable only
through the tunnel or LAN.
Verified fully end-to-end (phone on mobile data, real handshake + SSH login).
Several bugs only surfaced through live execution and were fixed in place:
a DNS=1.1.1.1 line that broke all phone DNS once the tunnel was active, a
require_root/sudo arg-forwarding bug, hostname/dig not being installed on a
minimal Arch system, a bash RETURN-trap scoping bug, and a DuckDNS cron-dedup
that would have deleted an unrelated pre-existing Joplin DuckDNS cron entry.
Also whitelists the WireGuard/F-Droid/ConnectBot apps (plus the todo app) in
phone_focus_mode's WHITELIST so the GPS-based focus daemon doesn't disable
them. Adds "iif" (nftables keyword) to the codespell ignore-list since it
was flagged as a false-positive typo of "if".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUSBRyujRMuGiUitGP8gET
Split diet_guard/_gatelock.py, wake_alarm/_alarm.py, and the
usage_report.py/_usage_report_parsing.py pair into focused
sub-modules so every Python file is <= 500 lines, satisfying
test_file_length.py. Install python-kasa into .venv (declared in
requirements but missing after the 3.13->3.14 venv upgrade),
fixing 8 failing smart_plug tests and restoring 100% coverage.
Also includes prior in-progress work from the working tree: the
wake_alarm Progress/View/Hardware field-grouping refactor,
brother_printer query module + tests, diet_guard foodbank/state/cli
updates, new shared coerce/logging_setup helpers, morning_routine
orchestrator tweaks, dwm window-manager config, gaming scripts, and
misc maintenance/digital-wellbeing script updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deleted screen_replay.sh (x11grab ring-buffer) and kill_stale_recorders.sh
(stale-ffmpeg killer). Removed autostart exec and Mod+Shift+F12 binding from
i3/config; removed replay startup from dwm-session. Both scripts archived in
testsAndMisc-archive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
turn_off_auto_idle_screen_shutdown.sh --watch-controller forked 4 xset +
1 xdotool per joystick event (blocking on a dd read, debounced by sleep
0.3) — ~21 procs/s while a controller was connected during gaming. The
xset poking was redundant: startup already disables DPMS/blanking.
Replace reset_idle_activity / watch_js_device / the polling watcher with
one long-lived `systemd-inhibit --what=idle:sleep` held only while a
/dev/input/js* device is present, re-evaluated on udev input add/remove
events (event-driven; 30s presence-poll fallback). EXIT+INT/TERM traps
release the inhibitor on every termination path.
Verified live: subtree stays {systemd-inhibit, udevadm} with zero
dd/xset/xdotool; exactly one inhibitor held; clean release on SIGTERM,
no orphans. Takes that loop from ~21 forks/s to 0.
Behavior change: keeps the session awake while a controller is connected
(not only during active input).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
atop's `-P PRC` output inserts the clock-tick rate (HZ=100) between the
`state` and `utime` columns. Both the Python parser and the native C
aggregator read that constant as utime for every record, charging a flat
1 CPU-second per record — so cpu_seconds collapsed to pid_count and
short-lived fork-storm commands (xset, dd, chronyc) topped the CPU table
(xset showed 67h). The old test fixtures lacked the HZ field, so code and
tests agreed on the bug.
- _parse_prc / atop_agg.c: read utime/stime past the HZ field (after+2/+3,
tokens[10]/[11]); bump the length guards accordingly
- restore C/atop_agg (deleted in 89b4f59) under linux_configuration/C/,
where the build path resolves; corrected test fixtures to include HZ
- _atop_agg_binary: fall back to the Python parser when the C source tree
is gone instead of trusting an orphaned cached binary
- add regression tests proving HZ is not summed as CPU
- bundle the in-progress since-last-report multi-day aggregation (segments,
-b/-e bounding, persisted state, window merging) and its tests/conftest
- meta: gate linux_configuration/tests in pytest_changed_packages.py
Verified by running usage_report.py --date 20260604: Top CPU now led by
SkyrimSE; xset/dd/chronyc fall to ~0. C unit tests + full pytest suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- install.sh: install sleep-hook.sh to systemd-sleep hooks (step 3) and
shutdown-wrapper.sh to /usr/local/bin/shutdown (step 5)
- shutdown-wrapper.sh: new script that intercepts shutdown calls and
redirects to rtcwake -m disk on alarm nights (Mon/Fri/Sat/Sun), pass-
through for reboots and cancel commands
- sleep-hook.sh: new systemd-sleep hook that restarts wake-alarm.service
for each logged-in user after hibernate resume
- setup_midnight_shutdown.sh: check wake-alarm day; if yes set RTC alarm
and hibernate, otherwise fall back to systemctl poweroff
- linux_configuration/tests: update script paths after periodic_background/
reorganisation (hosts_file_monitor, makepkg_capped, music_parallelism,
shutdown_timer_monitor, usage_monitoring_installer_efficiency)
- test_i3blocks_efficiency.sh: remove checks for HEARTBEAT_INTERVAL_S and
WARP_POLL_INTERVAL_S constants that no longer exist
- test_pacman_wrapper_security.sh: remove tests 20-21 (builtin time helpers /
external date calls) that are no longer applicable; update path
- generate_hosts_file.sh: add sed unblock rules for delio.com.pl and
loverslab.com to stay consistent with install.sh whitelist
- steam_backlog_enforcer/scanning.py: remove unplayable_reason arg from
logger.info call (too many format args); drop matching test assertion
- steam_backlog_enforcer/tests/test_protondb.py: add
test_unplayable_reason_no_trending_tier to restore 100% branch coverage
on protondb.py line 97 (was previously covered indirectly)