The brief's prescribed safety net for this file DOES NOT WORK, and that is
the main finding here. It specifies asdict + json.dumps(default=str) +
sha256 against baseline 1624750c. Four policy fields are frozenset, which
json cannot encode, so default=str stringifies them in SET ITERATION ORDER
-- random per process. Three consecutive runs gave three different digests;
pinning PYTHONHASHSEED=0 gave one. The recipe can never match a recorded
baseline, so it would have flagged every split as a regression regardless
of correctness.
The repo already ships a deterministic serializer. Before trusting it I
checked it was not a weaker proxy: policy_to_json emits all 16 dataclass
fields plus 6 more, so it is strictly stronger than asdict. Baseline
83e05e82, unchanged through all six extractions.
Two audits ran BEFORE any extraction:
- the loader regex-scans config.sh's TEXT ALONE, so the fifteen names it
reads are pinned here. Moving one does not error -- it silently yields
an empty set, and for WHITELIST that reads as "hide everything".
- extractions are variable-scoped, never line-range. A line-range attempt
cut through the WHITELIST string, leaving an unterminated quote; 15
variables vanished from the environment WHILE THE FINGERPRINT STILL
MATCHED, because the loader's regex tolerated the mangled file. That is
why the environment diff exists alongside the hash.
The environment check earned its keep twice: it also caught STATE_DIR being
left behind the source line, which expanded every path against an empty
prefix (/status.json rather than the real path) with the policy hash green
throughout. 79 variables, byte-identical either side, and every parsed list
compared entry-by-entry: 29/16/46/1/1/2/15, all matching.
Rationale prose moves to docs/policy-lists.md with pointers left behind,
including the blocks inside the list strings -- the loader and the shell
both ignore '#' lines there.
All five siblings are in both deploy lists. deploy.sh still sources the
chain from the checkout, run_phone.sh --help exits 0, 379 shell assertions
pass across nine suites, and focus_policy's 137 pytest cases pass.
config.sh is at EXACTLY 250 with no headroom; 184 of those lines are
loader-pinned lists that cannot move. Over-cap count 18 -> 17.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
|
||
|---|---|---|
| .. | ||
| docs | ||
| focus_status_app | ||
| lib | ||
| systemd | ||
| backup_manifest.sh | ||
| batch3_bloatware_uninstall.sh | ||
| config_curfew.sh | ||
| config_dns.sh | ||
| config_launcher.sh | ||
| config_paths.sh | ||
| config_tether.sh | ||
| config.sh | ||
| curfew_enforcer.sh | ||
| curfew_net.sh | ||
| daemon_apps.sh | ||
| daemon_location.sh | ||
| daemon_state.sh | ||
| deploy.sh | ||
| distraction_purge.sh | ||
| dns_enforcer.sh | ||
| dns_iptables.sh | ||
| enable_persistent_adb.sh | ||
| focus_ctl.sh | ||
| focus_daemon.sh | ||
| hosts_enforcer.sh | ||
| hosts_magisk.sh | ||
| hosts_mount.sh | ||
| launcher_enforcer.sh | ||
| magisk_service.sh | ||
| phone_backup.sh | ||
| README.md | ||
| restore_apps.sh | ||
| run_phone.sh | ||
| strip_workout_hosts.py | ||
| tether_enforcer.sh | ||
| tether_iptables.sh | ||
| workout_detector.sh | ||
Phone Focus Mode
Location-based app restriction for a rooted Android phone using wireless ADB.
When within ~500m of home: only whitelisted productive apps remain usable. When outside that radius: all apps work normally.
Requirements
- Rooted phone with Magisk installed
- Wireless ADB enabled (
Settings → Developer options → Wireless debugging) adbinstalled on your PC (sudo apt install adbon Debian/Ubuntu)- GPS/Location enabled on the phone
Setup (first time)
1. Find your home coordinates
Open Google Maps, right-click your apartment → copy the coordinates shown.
2. Edit config_secrets.sh
HOME_LAT="-48.876667" # your latitude
HOME_LON="-123.393333" # your longitude
3. (Optional) Adjust the whitelist in config.sh
To find the exact package name of any app:
./deploy.sh <phone_ip> --find-pkg stronglift
./deploy.sh <phone_ip> --find-pkg anki
./deploy.sh <phone_ip> --find-pkg pomodoro
Then add the correct package name to WHITELIST in config.sh.
4. Deploy
chmod +x deploy.sh
./deploy.sh 192.168.1.42 # replace with your phone's IP
This:
- Pushes all scripts to
/data/local/tmp/focus_mode/on the device - Installs a Magisk
service.dscript so the daemon auto-starts on boot - Starts the daemon immediately
Usage
./deploy.sh <ip> --status # Current mode, location, distance from home
./deploy.sh <ip> --log # View recent daemon log
./deploy.sh <ip> --list # List all apps + whitelist status
./deploy.sh <ip> --enable # Force focus mode ON (for testing)
./deploy.sh <ip> --disable # Force focus mode OFF
./deploy.sh <ip> --stop # Stop daemon entirely (restores all apps)
./deploy.sh <ip> --start # Start daemon
./deploy.sh <ip> --restart # Restart daemon (picks up config changes)
./deploy.sh <ip> --pull-log # Download log file to your PC
How it works
Every 60 seconds:
get_location() ─── dumpsys location ──► lat,lon
│
▼
calc_distance() ─── Haversine formula ──► meters
│
├── within radius? ──► enable_focus_mode()
│ pm disable-user all non-whitelisted apps
│ record which apps were disabled
│
└── outside radius? ──► disable_focus_mode()
pm enable each app in the disabled list
Hysteresis: 50m buffer prevents rapid toggling at the boundary. You must travel
radius - 50m inward to trigger lock, and radius + 50m outward to unlock.
Fail-safe: If location is unavailable for 5 consecutive checks (~5 minutes), focus mode is automatically disabled so you can't be locked out.
State persistence: The daemon records exactly which apps it disabled
(in /data/local/tmp/focus_mode/disabled_by_focus.txt), so it never accidentally
re-enables apps that were already disabled by the user before focus mode ran.
On-device control (without PC)
From a root terminal app (e.g. Termux + tsu):
su --mount-master -c 'sh /data/local/tmp/focus_mode/focus_ctl.sh status'
su --mount-master -c 'sh /data/local/tmp/focus_mode/focus_ctl.sh disable'
Why --mount-master: MagiskSU puts each su -c session in an isolated
mount namespace by default, so bind mounts made by the hosts enforcer would be
invisible (and /data/adb/focus_mode/* checks would fail due to SELinux
interactions). --mount-master joins the global namespace where the daemons
(started from Magisk service.d at boot) actually live. The boot autostart
script doesn't need this flag because post-fs-data already runs there.
File layout
| File | Purpose |
|---|---|
config.sh |
Coordinates, radius, whitelist, constants |
focus_daemon.sh |
Main daemon — runs on device, loops every 60s |
focus_ctl.sh |
Control utility — runs on device |
curfew_enforcer.sh |
Night-curfew enforcer — grayscale + DND + optional net |
hosts_enforcer.sh |
Bind-mounts hosts.canonical over /system/etc/hosts |
magisk_service.sh |
Magisk boot hook → auto-starts both daemons |
deploy.sh |
PC-side ADB deployment and control script |
Hosts hardening
A second daemon, hosts_enforcer.sh, locks the phone's /system/etc/hosts
to the same blocklist installed by linux_configuration/hosts/install.sh
on the PC. Three layers:
- Canonical copy at
/data/adb/focus_mode/hosts.canonicalischattr +i. - It is bind-mounted read-only over
/system/etc/hostsat boot. - A watchdog verifies a sha256 every 15 seconds and restores on mismatch.
This blocks the common echo > /etc/hosts one-liner from a terminal app.
It is NOT a guarantee against a determined root user on the device itself —
a real "impossible without USB" gate would require removing su access,
which would break the rest of this system. The watchdog at least ensures
tampering is logged and reverted within ~15s.
Status and logs:
./deploy.sh <ip> --hosts-status
./deploy.sh <ip> --hosts-log
Periodic rescan / Play Store
The focus daemon now re-scans every tick (not just on first entry). If
you re-enable an app via Play Store or pm enable, it gets re-disabled
within CHECK_INTERVAL_FOCUS seconds. com.android.vending (Play Store),
com.*.packageinstaller, and popular terminal apps are also uninstalled
--user 0 in focus mode to close the usual bypass paths. Google Play
Services (com.google.android.gms) is left alone so banking apps work.
More detail
Split out to stay under the 250-line cap.