The bind-mount logic goes to hosts_mount.sh, the Magisk-module and
cache-invalidation logic to hosts_magisk.sh. Both flat siblings, both in
both deploy lists; the two lists were then checked to correspond exactly
in both directions, since a push with no matching cp stages a file that
never lands and the enforcer would fail to source it.
Audited for stdin-draining `while read` loops BEFORE extracting, because
piping one would change behaviour in a way stubs cannot catch. There are
none here. Also confirmed no function writes a global, so there was no
cross-half hazard of the kind that kept NET_BUILT in curfew_enforcer.sh.
69 assertions, 43/43 and 62/62 lines, 17 mutations all killed. Two survived
the first run and both were real findings:
- the unmount give-up threshold was not pinned. Raising it from 5 to 500
changed nothing observable, so the enforcer could have spun 500 times
per poll against a mount that was never going to clear. Now asserted as
a call count.
- the `[ ! -f $HOSTS_TARGET ] && return 1` guard in
is_bind_mounted_correctly is PROVABLY EQUIVALENT: sha256_of returns
empty for a missing file, so the existing -n check already rejects that
case. No test can distinguish it. Equivalent code is a claim of a check
that is not being made, so it is deleted rather than covered by a test
that would pass either way. Old and new bodies were compared across all
four input states -- both-missing, target-missing, match, mismatch --
and agree on every one.
That deletion is the only non-verbatim function; the other 19 moved
untouched, making this the first fully verbatim extraction of the session.
Ordering is asserted where it is load-bearing: /system must go back to ro
after rw, and +i must be restored after being dropped. Presence assertions
alone would pass with either pair reversed.
UNVERIFIED ON DEVICE -- the highest-stakes of the four enforcer splits now
outstanding, since a failure to source leaves nothing blocking while the
phone looks normal. Over-cap count 20 -> 19.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
"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
- Added restart_netd_for_hosts_cache() to hosts_enforcer.sh with PID-stamp
deduplication to prevent double-restarts across enforcer invocations
- Removed explicit netd restart from deploy.sh (caused double-restart
that broke ConnectivityService binder link and dropped default route)
- deploy.sh: wait 10s after starting focus_daemon.sh for enforcer to
complete its single netd restart before companion app install
- Misc updates to dns_enforcer.sh and config.sh
The Magisk app's Modules tab "Disable" / "Remove" buttons work by
creating marker files (disable, remove) in /data/adb/modules/hosts/.
Tapping Disable in the app on next boot would skip the module's
magic-mount of /system/etc/hosts, silently disabling all hosts-file
blocking.
Defense in depth:
1. deploy.sh chattr +i's the module dir + its hosts file so the
Magisk app cannot create disable/remove markers (kernel returns
EPERM). The +i attribute survives reboot.
2. hosts_enforcer.sh adds protect_magisk_module(): every poll cycle
(and on startup) scans for disable/remove/update markers, deletes
them, logs TAMPER, and re-asserts +i on the dir. Safety net in
case the lock is bypassed.
3. sync_magisk_module() now drops +i briefly before its cp and
re-locks via protect_magisk_module() so workout-state hosts
swaps still work.
4. deploy.sh detects the previously-silent failure mode of the
module being enabled on disk but not yet magic-mounted (no
/system/etc/hosts) and aborts with a clear reboot-required
message instead of producing a deploy that does nothing.
5. focus_ctl.sh hosts-status now prints the lock state and warns
about any present markers.
Verified end-to-end on BL9000EEA0000102:
- Pre-reboot: chattr +i set, touch /data/adb/modules/hosts/disable
returns Operation not permitted.
- Post-reboot: /system/etc/hosts magic-mounted (178303 lines, sha
matches canonical), lock survives reboot, ping youtube.com -> 127.0.0.1.
- Tamper test: chattr -i + touch disable -> enforcer logs
'TAMPER: removed Magisk module marker' within 15s and re-locks.
Documented intentional override path inline (focus_ctl.sh hosts-stop;
chattr -i; touch disable).
- New companion Android app (com.kuhy.focusstatus) under
phone_focus_mode/focus_status_app/ with a pure-Java, Gradle-less
command-line build pipeline (build.sh). Shows an ongoing
notification titled 'Focus: HOME / AWAY / DAEMON DOWN' with
distance, GPS, disabled-app count, last check, daemon checkmarks,
and a 'Re-check now' action button.
- focus_daemon.sh: write_status_snapshot() + sleep_with_recheck()
for JSON status + early-wake on trigger file. init() chmods
STATE_DIR 777 so the app can drop the trigger file.
- config.sh: new STATUS_FILE / RECHECK_TRIGGER; WHITELIST expanded
with com.kuhy.focusstatus and 11 more user-requested apps
(podcini X, mpv, bible/openbible, pkp/portalpasazera, orange,
runnerup, splitbills/splitwise, xiaomi smarthome).
- focus_ctl.sh: new 'recheck' + 'notif-status' subcommands.
- deploy.sh: new step [7/7] builds APK, installs, grants
POST_NOTIFICATIONS, pre-approves Magisk SU policy, launches
foreground service.
- .gitignore: exclude focus_status_app/build symlink + debug.keystore.
End-to-end verified on device: notification live with real values;
Re-check button triggers a daemon location check within ~1s.