Files
SellPL aa1c582f3c chore: add repo hygiene config and project spec
Bootstrap the repository with the shared configuration every other piece
depends on:

- .gitignore: strict. Keeps build output, cargo caches, Android keystores,
  signing keys, .env files, local SQLite databases, IDE state and any
  credential-shaped filename out of history. CLAUDE.md 10.1 requires these
  never enter the repo.
- .editorconfig, rustfmt.toml, clippy.toml: formatting and lint policy.
  clippy.toml is written in the lints-file form so lints are not duplicated
  into every crate manifest.

CLAUDE.md is committed verbatim; it is the project spec and the parity
contract for every platform layer.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-02 13:10:25 +00:00

23 KiB
Raw Permalink Blame History

CLAUDE.md — "Blocker": cross-platform app & website blocker

Functional equivalent of AppBlock (appblock.app) for Android, Windows, Linux. Goal: same behavior, near-zero resource usage. Own name, own branding, own assets.

0. Rules for the agent (read first)

  1. Functional parity, not visual copying. Do not reuse AppBlock's name, logo, icons, copy, or screenshots. Replicate behavior only.
  2. Never fake a feature. If a platform cannot enforce something (see §6), implement the closest honest approximation, document the gap in docs/LIMITATIONS.md, and tell me. Do not silently stub.
  3. Stop and ask before: adding any dependency not listed in §3, changing the architecture in §2, or choosing between two non-obvious OS mechanisms.
  4. Work in milestones (§8). Finish one, run its acceptance checks, then stop and report. No jumping ahead.
  5. Every OS call that can fail must be handled. No unwrap() / !! on OS or I/O results outside tests.
  6. Performance is a feature (§5). Measure it; don't assume it.
  7. Everything goes through git on my Gitea (§10). Never work outside a branch; never commit to main directly.
  8. Turning blocking off must be deliberately painful (§1.5), but never impossible and never unsafe. The friction is opt-in, chosen by the user at activation time, always finite, and never touches the safety exemptions in §1.5.4.

1. Feature spec (target: 1:1 behavior)

1.1 Schedules (the core object)

A schedule =

  • name + icon (user-chosen)
  • targets: apps, websites, keywords
  • one or more conditions (see 1.2)
  • mode: Blocklist (listed items blocked) or Allowlist (only listed apps allowed; apps only, not websites)
  • enabled / paused state

Rules:

  • If multiple schedules touch the same target, strictest wins: blocked while ≥1 schedule wants it blocked.
  • Schedules are editable at any time unless Strict Mode is active.
  • Ship starter templates: Focus, Wind Down, Mindful Morning, Study Time, At Home, Digital Detox.

1.2 Conditions

Condition Behavior
Time days of week + start/end time (must support overnight ranges)
Usage limit block after N minutes per day or per hour
Launch count block after N opens per day or per hour
Location block inside (or outside) a radius of a point — mobile only
Wi-Fi block while connected to a chosen SSID

Combination rules (matches AppBlock; enforce in validation):

  • Time + usage limit: not allowed
  • Time + launch count: not allowed
  • Usage limit + launch count: not allowed
  • Usage limit + location, launch count + location: not allowed
  • Use separate schedules to combine these (strictest-wins handles it).

1.3 Blocking behavior

  • Apps: when a blocked app comes to the foreground, show a block screen immediately and send the user away from it.
  • Websites: block domains and keyword matches in browsers.
  • QuickBlock: one-tap immediate block of a chosen set for a chosen duration.
  • Pause / break: temporarily pause a schedule (disabled or restricted under Strict Mode).
  • Block screen: shows which schedule blocked it and when it ends; no bypass button while Strict Mode is on.

1.4 Strict Mode

Requires at least one schedule or QuickBlock to exist. When active, the user cannot:

  • edit or delete any schedule (even if Strict Mode was tied to one — all are locked)
  • pause schedules / QuickBlock
  • uninstall the app (Android: Device Admin; desktop: see §6)
  • open device settings pages that would let them disable enforcement (configurable)

Configurable parts:

  • which restrictions to lock (checklist)
  • unlock condition chosen at activation time (timer end, cooldown, etc.)
  • cooldown: 2–10 minute wait on the Strict Mode screen before deactivation
  • optional "block uninstall" even when Strict Mode is off

1.5 Friction: making it a pain to turn off

Design goal: disabling blocking in a weak moment must cost enough time and effort that the urge passes, while a determined, calm decision still gets through eventually. This goes beyond AppBlock's Strict Mode and is the main differentiator of this project.

1.5.1 Lock levels — chosen at activation, shown with a plain-language description of exactly what it will cost to undo:

Level Disable flow
Normal edit/pause freely
Strict AppBlock-equivalent Strict Mode (§1.4)
Stubborn Strict + the full friction ladder below

Rules for levels:

  • A level can be raised instantly but lowered only by going through that level's disable flow.
  • The user must see and confirm the exact disable cost before activating ("To undo this you will wait 24h and retype a 300-character code").

1.5.2 The friction ladder (each step is individually configurable; Stubborn enables all):

  1. Cooldown: configurable wait (default 10 min, up to 30) on the disable screen. Leaving the screen resets it.
  2. Time-lock request: disabling doesn't disable; it files a request. Blocking stays fully active for the configured delay (default 12 h, max 24 h). When the delay ends, a short confirmation window opens (e.g. 10 min). Missing the window cancels the request and starts over. The user can cancel the request any time.
  3. Typing challenge: retype a random string (default 300 chars, mixed case + digits). Paste disabled, no autofill/suggestions, and any typo resets it. Regenerate a new string on each attempt.
  4. Escalation: each abandoned or failed attempt doubles the next cooldown (cap at 30 min per step; resets after 7 days with no attempts).
  5. Attempt log: every disable attempt, with timestamp, is shown to the user on the block screen ("You tried to disable this 4 times today"). Local only.
  6. No shortcuts: no notification action, quick tile, widget, or tray menu item that disables, pauses, or opens the disable flow. The flow lives only inside the app, behind a plain-looking entry.
  7. Optional keyholder: at activation the app generates a one-time unlock code and shows it once so the user can hand it to someone they trust (or print it and give it away). Entering it skips steps 1–4. If the user declines this option, the ladder still applies.

1.5.3 Anti-tamper requirements (the engine must enforce these)

  • All delay timers use a monotonic clock (Android elapsedRealtime, CLOCK_MONOTONIC, QueryPerformanceCounter). Changing the system date/time must never shorten a delay. If a wall-clock jump is detected, log it and extend the active lock, never shorten it.
  • Timer state survives reboot (persist elapsed-so-far with a boot ID; if reboot detected, resume conservatively).
  • Force-stopping, killing, or disabling the enforcer is detected and repaired where the OS allows (§4, §6). Platforms that can't prevent it must still log it and re-apply blocks on next start.
  • Schedule/Strict state is stored in the core DB, not in editable plaintext config.

1.5.4 Safety guardrails (non-negotiable, these protect the user)

  • Never-block list, hardcoded and not user-removable from the blocklist: phone/dialer, emergency calls/SOS, emergency alerts, and on desktop the OS's own shell and Settings basics needed to keep the machine usable. Surface them in docs.
  • Finite worst case: the longest possible disable path is capped at 24 h of waiting. Never design a lock with no exit.
  • Factory reset, safe mode, and OS-level recovery are never interfered with. Do not attempt to block them.
  • Opt-in only: Stubborn/Strict are never enabled by default or by a schedule template; the user turns them on explicitly with the cost shown.
  • The disable flow must be documented in-app (a plain "How to turn this off" page reachable without disabling anything), so the user is never stuck wondering.
  • If the app itself malfunctions (service crash loop, corrupt DB), fail open after logging, never leave the device unusable.

1.6 Statistics

Per-app and per-website usage tracking (daily/weekly), computed locally.

1.7 Settings

Dark/light theme, behavior when location is unavailable, which settings stay accessible under Strict Mode, troubleshooting screen (shows which permissions/services are missing).

1.8 Out of scope for v1

iOS, cloud accounts, cross-device sync, billing/premium tiers (all features are free), browser extensions beyond the §6 website mechanism. Design the storage so sync could be added later; do not build it.

2. Architecture

One shared core holds all decision logic so behavior is identical on every OS. Platform code only (a) observes the OS and (b) enforces decisions.

┌────────────────────────── core (Rust, no_std-friendly where possible) ──────────────────────────┐
│ model: Schedule, Condition, Target, StrictConfig                                               │
│ engine: evaluate(state, now, events) -> Decision{blocked set, reasons, next_wakeup}           │
│ storage: SQLite (rusqlite, bundled) — schedules, usage counters, settings                      │
│ usage accounting: foreground-time & launch-count aggregation                                   │
└──────────────▲───────────────────────────────────────────────────▲───────────────────────────┘
               │ uniffi (Kotlin bindings)                           │ direct Rust crate
        ┌──────┴───────┐                              ┌─────────────┴──────────────┐
        │ android/     │                              │ desktop/ (win + linux)     │
        │ Kotlin shell │                              │ daemon + small UI          │
        └──────────────┘                              └────────────────────────────┘

Key principle: the engine is a pure function of (schedules, usage state, wall clock, context like SSID/location). No OS calls inside it. This makes it unit-testable and guarantees parity.

Engine must be event/alarm driven, never polling. It returns next_wakeup (the next time any decision could change); the platform layer sleeps until then or until an OS event arrives.

Repo layout:

/core            Rust crate (engine, model, storage, usage)
/android         Kotlin app (thin: services, overlay, UI)
/desktop         Rust workspace: /daemon, /ui, /platform-windows, /platform-linux
/docs            ARCHITECTURE.md, LIMITATIONS.md, PERMISSIONS.md, DECISIONS.md, PERF.md, TESTING.md
/tests           cross-platform scenario tests (golden decision tables)
/scripts         ci.sh (same checks as CI, runnable locally), size-check, release helpers
/.gitea          workflows/, issue + PR templates

3. Language & dependency choices

Layer Choice Why
Core + desktop Rust no GC, tiny idle footprint, one codebase for logic
Android shell Kotlin (no Compose, no DI framework, no Jetpack extras beyond what's required) The blocking APIs (AccessibilityService, UsageStatsManager, DevicePolicyManager, overlays) are JVM-only; a non-JVM language can't register these services. The shell stays thin, so performance is dominated by the Rust core
Core ↔ Android uniffi (or hand-written JNI if uniffi bloats size — ask)
Storage rusqlite with bundled SQLite
Desktop UI slint (preferred) or egui — decide in M3 after a size/RAM test, ask me far lighter than Electron/Tauri webviews
Windows API windows crate
Linux x11rb (X11), zbus (D-Bus/logind/NetworkManager), nix

Rules: no Electron, no webview, no Flutter, no React Native, no JVM on desktop. Prefer std over crates. Every added crate must justify its binary-size cost.

4. Platform enforcement design

4.1 Android

  • Foreground detection: AccessibilityService (TYPE_WINDOW_STATE_CHANGED) — event-driven, no polling. Use UsageStatsManager only for historical/usage stats and as a fallback.
  • Block action: launch block screen activity / overlay (SYSTEM_ALERT_WINDOW) and send user Home.
  • Websites: Accessibility node reading of the browser URL bar for known browsers (keep a per-browser resource-id table, make it updatable); match domains + keywords.
  • Strict Mode / uninstall protection: DeviceAdminReceiver for uninstall block; Accessibility service intercepts Settings screens that would disable the service.
  • Location / Wi-Fi: Geofencing API for location; ConnectivityManager network callback for SSID.
  • Reliability: foreground service with persistent notification, battery-optimization exemption prompt, BOOT_COMPLETED receiver, OEM autostart guidance in the troubleshooting screen.
  • Permissions screen guides the user through: Usage access, Accessibility, Display over other apps, Battery optimization, Location (optional), Autostart (OEM-specific).
  • Play Store note: Accessibility and Device Admin use require policy justification. Also provide a sideload/F-Droid build flavor.

4.2 Windows

  • Foreground detection: SetWinEventHook(EVENT_SYSTEM_FOREGROUND) — event-driven. Map HWND → PID → exe path (and package identity for UWP apps).
  • Block action: show a topmost block window and minimize/close the offending window; terminate the process only as an escalation, configurable.
  • Usage/launch counting: from the same foreground events + EVENT_OBJECT_CREATE/process start via WMI or ETW (choose lightest, ask me).
  • Runs as: per-user background daemon at login + a Windows Service watchdog that restarts it.
  • Websites: see §6 (this is the weak point).
  • Wi-Fi: WLAN API notification callback.

4.3 Linux

  • X11: _NET_ACTIVE_WINDOW property-change events via x11rb → _NET_WM_PID → /proc/<pid>/exe.
  • Wayland: there is no portable global API for the focused window. Implement compositor-specific backends behind a trait: wlroots foreign-toplevel-management, KDE (KWin script/D-Bus), GNOME (extension). Fallback: process-based blocking — watch process creation (netlink proc connector, needs privilege) and match by executable.
  • Block action: overlay window (X11) / layer-shell (wlroots) / terminate process as fallback.
  • Runs as: systemd --user service; optional system service for tamper resistance.
  • Wi-Fi: NetworkManager via D-Bus.

5. Performance budgets (must be measured and reported per milestone)

Metric Android Windows / Linux daemon
Idle CPU ~0% (no wakeups without events) ~0%, ≤ 1 wakeup/min when idle
Idle RAM ≤ 30 MB PSS ≤ 15 MB RSS (daemon), UI process closed when window closed
Block latency (foreground change → block shown) ≤ 150 ms ≤ 150 ms
Daemon binary — ≤ 5 MB stripped (opt-level="z", LTO, panic="abort", strip)
APK size ≤ 10 MB (release, R8) —
Battery no wakelocks outside active enforcement —

How to measure: adb shell dumpsys meminfo, Perfetto/battery stats (Android); psutil/perf/heaptrack (Linux); Process Explorer/ETW (Windows). Put results in docs/PERF.md.

6. Known hard limits (tell me, don't hide them)

  1. Desktop Strict Mode cannot be tamper-proof against an admin/root user. Mitigations: watchdog service, file ACLs / immutable flag on config, restart-on-kill, tamper log. Document honestly in LIMITATIONS.md. Do not claim "unbypassable."
  2. Desktop website blocking has no single clean mechanism. Evaluate and report options before building: (a) local DNS resolver / hosts-file management, (b) browser extension via native messaging (needed for keyword and path-level blocking, and HTTPS-aware), (c) both. DNS-only cannot do keyword blocking or defeat DoH. Ask me which to ship.
  3. Wayland focused-window detection is compositor-dependent (§4.3).
  4. Location schedules are mobile-only (no reliable desktop equivalent); hide that condition on desktop.
  5. Android OEMs (Xiaomi, Huawei, Samsung, etc.) kill background services aggressively; troubleshooting screen must cover this.
  6. Android Accessibility URL reading breaks when browsers change their UI; the resource-id table needs a maintenance path.
  7. "Pain to turn off" is friction, not a wall. A user with ADB, root, safe mode, a factory reset, or admin rights on desktop can always remove the app. That's correct and must stay true (§1.5.4). The goal is to outlast an impulse, not to defeat a determined person. Do not try to block ADB, safe mode, or recovery.
  8. Play Store policy is hostile to apps that resist uninstall. Keep the strongest friction (Stubborn level, Device Admin uninstall block) behind the explicit opt-in and plan on a sideload/F-Droid flavor as the primary distribution.

7. Testing

  • Engine: property tests + golden decision tables in /tests — same table runs against the core and is the parity contract for all platforms. Cover: overnight time ranges, DST changes, timezone changes, clock tampering (use monotonic time for usage accounting), strictest-wins, allowlist mode, invalid condition combos, Strict Mode lock rules.
  • Platform layers: manual test checklist per OS in docs/TESTING.md plus automated tests where feasible (Android instrumented tests, Linux Xvfb tests).
  • A milestone isn't done until its acceptance checks pass and perf numbers are recorded.

8. Milestones

M0 — Repo bootstrap (git first). Create the repo on my Gitea, set up layout, .gitignore, scripts/ci.sh, CI workflow skeleton, issue/PR templates, docs/DECISIONS.md. File every §6 limitation as an issue. Accept: clean clone builds an empty workspace; CI (or scripts/ci.sh if no runner) passes on main.

M1 — Core engine (no UI, no OS). Model, evaluator, storage, usage accounting, golden tests. Accept: all decision-table tests pass; evaluate is pure and has no platform imports.

M2 — Android MVP. Permissions flow, foreground detection, app blocking by Time condition, block screen, QuickBlock. Accept: block latency ≤ 150 ms, idle CPU ~0%, survives reboot.

M3 — Desktop daemon + UI (Linux/X11 first, then Windows). Daemon with app blocking by Time, block window, minimal UI. Pick Slint vs egui here with measured RAM/size. Accept: §5 budgets met.

M4 — All conditions. Usage limit, launch count, Wi-Fi, Location (mobile), validation of combination rules, templates, pause behavior.

M5 — Websites & keywords. Android via Accessibility; desktop via the mechanism I choose from §6.2.

M6 — Strict Mode + friction ladder (§1.4, §1.5). Lock rules, lock levels, cooldown, time-lock request, typing challenge, escalation, attempt log, keyholder code, monotonic-clock anti-tamper, never-block list, in-app "How to turn this off" page, uninstall protection (Android Device Admin), desktop watchdog. Write the honest limitations doc. Accept: engine tests prove that changing the system clock never shortens a lock, a reboot never resets one, the worst-case disable path is ≤ 24 h, and the never-block list can't be blocked.

M7 — Statistics, settings, troubleshooting screen, Wayland backends, release builds, perf report.

9. Definition of done (per feature)

  • Behavior matches §1 on every platform where it's supported
  • Golden tests updated
  • No new idle wakeups introduced
  • Perf budget table re-measured if the feature touches the hot path
  • Limitations documented where the platform can't comply

10. Git workflow (everything goes through git)

Remote: my self-hosted Gitea at https://git.valiq.vu (Gitea 1.26, so Gitea Actions and the REST API at /api/swagger are available).

Before first push, ask me for: the owner/org and repo name, whether to use SSH or an access token, and whether a Gitea Actions runner (act_runner) is registered. Never guess these. Never commit tokens, keys, or keystores; read credentials from the environment or my SSH agent.

10.1 What lives in git

Everything that defines the project: source, golden test tables, CI config, scripts, docs, architecture decisions (docs/DECISIONS.md), perf measurements (docs/PERF.md), the permission/limitation docs, issue and PR templates.

What does not: build outputs, APKs/binaries (attach those to Gitea Releases instead), Android keystores, .env files, local SQLite databases, IDE junk. Keep .gitignore strict, and run a secret scanner (e.g. gitleaks) in scripts/ci.sh and CI.

10.2 Branching and commits

  • main is always green and releasable. No direct commits to main.
  • One short-lived branch per task, named <milestone>/<topic>, e.g. m2/accessibility-foreground, m6/time-lock.
  • Small, atomic, conventional commits (feat:, fix:, perf:, refactor:, test:, docs:, ci:). Each commit must build and pass tests on its own.
  • Open a pull request on Gitea for each branch (via the API or tea CLI) with: what changed, which §1 requirement it covers, test evidence, and perf numbers if it touches a hot path.
  • I merge. The agent never merges its own PR without my explicit go-ahead. Use squash or rebase merge, keep history linear.
  • Tag each finished milestone (m1, m2, ...) and each release (v0.1.0, semver). Tags are annotated.

10.3 Issues as the backlog

  • File one Gitea issue per milestone (M0 to M7) with its acceptance checks as a checklist.
  • File one issue per §6 hard limit, labeled limitation, so none get forgotten.
  • Reference issues in commits and PRs (refs #12, closes #12).
  • Any open question for me becomes an issue labeled question instead of being buried in chat.

10.4 CI (Gitea Actions, .gitea/workflows/)

Every PR must pass:

  1. cargo fmt --check and cargo clippy -- -D warnings
  2. cargo test including the golden decision tables
  3. Size gate: fail if the stripped desktop daemon exceeds 5 MB (§5)
  4. Android: lint and assembleRelease (only if the runner has the SDK; ask me)
  5. Secret scan

scripts/ci.sh runs the same checks locally, so work is verifiable even if no runner exists. If I have no runner, write the workflows anyway and tell me what runner setup they need.

10.5 Releases

Tagged release builds attach artifacts to a Gitea Release: Android APK (sideload flavor), Windows .exe/installer, Linux binary + .service unit, each with a SHA-256 checksum file. Signing keys are never in the repo.

10.6 Agent hygiene

  • Run git status and git diff before every commit; no git add -A blindly.
  • Never rewrite or force-push shared history (main, tags). Force-push is allowed only on your own unmerged feature branch.
  • If a milestone's checks fail, say so in the PR; don't weaken a test to make it pass.