A total gaming block on 2026-07-04 purged the Steam client (packages and
all data dirs, per total_block_purge_log.json) and it was never
reinstalled. The enforcer kept trying to drive it anyway: every start
called ensure_steam_debug_port() -> _launch_steam_with_debug(), waited
45s for a CDP port no process would ever open, raised, and exited 1 into
Restart=always. That ran to 1050+ restarts.
Each attempt also left a zombie behind: _run_as_user() launches
fire-and-forget via Popen and never reaps, so a launch that dies on exec
lingers as a dead process still named "steam". The focus-mode daemon
scans /proc for process names and read that zombie as a live Steam,
entering GAMING mode and killing the user's browsers every few seconds.
- steam_is_installed(): probe the real /usr/bin/steam. Deliberately not
shutil.which("steam") - a launcher wrapper on $PATH keeps `which`
truthy long after the package is gone, which is precisely how a
deleted Steam went on looking installed.
- SteamUnavailableError (a RuntimeError, so existing handlers still
catch it) raised on both "not installed" and CDP timeout, and caught
at the _enforce_hide_games boundary: no library to hide is not fatal,
the store block and install guard still work.
- Guard _enforce_loop_iteration and game_install._ensure_steam_running,
whose 15s sleep was the source of the observed flapping interval.
- do_enforce falls through to the idle loop rather than returning:
returning exits the process, which under Restart=always is the same
crash loop by another name.
- _reap_spawned() retires exited launches so no zombie named "steam"
outlives them.
Verified live: NRestarts 1050 -> 0, service active/running for 60s+ with
zero errors and zero steam zombies, logging "Steam is not installed -
nothing to enforce."
Also removed the orphaned /usr/local/bin/steam wrapper, a leftover of
the purge that pointed at the deleted binary. Its body was:
#!/bin/sh
exec /usr/bin/steam -cef-disable-gpu -cef-disable-gpu-compositing "$@"
Restore those flags if a Steam reinstall brings back GPU trouble.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rN2usTS3pYLJqeVg2ZpgR
Whitelist exceptions previously entered a 24h pending queue and were
promoted to approved by the enforce loop once the cooldown elapsed.
add_pending_exception now approves immediately, so the entire pending/
promote/cooldown mechanism is dead: drop WHITELIST_COOLDOWN_SECONDS,
PENDING_EXCEPTIONS_FILE, _load_pending/_save_pending,
promote_pending_exceptions, and list_pending_exceptions, plus the
enforce-loop promotion step and the CLI's now-empty pending display.
Usage text and docstrings updated to state exceptions are active
immediately. Tests for the removed machinery deleted; add-exception
tests assert immediate approval. Coverage remains 100%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
Adds `block-gaming <days>`: uninstalls Steam, kills/uninstalls known game
launchers, and blocks Steam + game-website domains (hosts + iptables) for a
fixed number of days with no in-app way to lift it early. Enforcement is
tamper-resistant via guard-lib's package-block (bind-mounted lock file) and
re-asserted every enforce tick.
Also migrates store_blocker.py's hosts-file locking from raw chattr/mount
calls to guard-lib's file-guard, using the new `sync` subcommand (not
`pacman-relock`) so our own legitimate edits aren't reverted as drift.
Fixes found during live verification:
- iptables never blocked real IPs because DNS was resolved after /etc/hosts
already redirected every blocked domain to 0.0.0.0 locally - reordered so
iptables resolves first.
- Game-website blocks only covered bare apex domains; sites that
301-redirect to www (e.g. newgrounds.com) sailed right through - added
automatic www. variant generation.
- Launchers (e.g. prismlauncher) were only killed, never uninstalled -
added best-effort pacman-package removal keyed off /proc/<pid>/exe.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFNiYQQgSLAkiBXswyimPq
- Remove skip_app_ids from user-editable Config; callers updated
- Split PROTECTED_APP_IDS: only Steam infra/Proton IDs remain; game
IDs moved to a new time-locked exception system
- Add _whitelist.py: 24-hour cooldown on new exceptions, entropy-
checked justification (>= 5 words), append-only audit log,
chattr +i immutability on enforcement-critical config files
- Add is_protected_app() in game_install.py; used everywhere
instead of direct PROTECTED_APP_IDS membership checks
- Add 'add-exception' CLI command (cmd_add_exception in main.py)
- Call promote_pending_exceptions() and lock_enforcement_files()
in each _enforce_loop_iteration
- 590 tests, 100% branch coverage on all steam_backlog_enforcer modules
- Add .worktrees to .gitignore
- Guard enforce_allowed_game() and _guard_installed_games() against
current_app_id=None so they never treat all games as unauthorized
- Add early return in _enforce_loop_iteration when no game is assigned
- Wrap State.load() in enforce loop with error handling for corrupt files
- Switch all config/cache file writes to atomic (tmpfile + rename)
- Add robust error handling to State.load() for corrupt JSON
- Update tests for new behavior and add coverage for atomic writes