From f8b0d97ce415bddb0c222192a3f64c032c397535 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 16 Aug 2026 09:12:39 -0400 Subject: [PATCH] Revise backup plan after codex xhigh review Co-Authored-By: Claude Fable 5 --- plans/backup.md | 101 ++++++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/plans/backup.md b/plans/backup.md index 2224ac87..f29e5e6d 100644 --- a/plans/backup.md +++ b/plans/backup.md @@ -1,6 +1,6 @@ # Plan: Backup — off-site, incremental, encrypted, panel-controlled -Revision 1. +Revision 2. Rev 2 incorporates adversarial review by codex (xhigh): honest threat model, secrets moved out of the backup scope and out of `~/.config`, corrected timer/pause mechanics, reordered setup, and corrected restic claims (exclude expansion, exit codes, partial backups, lock contention, prune cost, fuse2). ## Problem @@ -8,19 +8,28 @@ Omarchy has no answer for "my disk died", "my laptop was stolen", or "I deleted - Snapper snapshots cover the root filesystem only, live on the same disk, and `manual/47-system-snapshots.md` says outright they don't recover personal files. - The dots plan (`plans/dots.md`) is config history + sync, and is explicit that it is *not* a backup — the local repo dies with the disk. -- The manual's file-safety story is "install Dropbox", which is sync, not backup: deletions and ransomware propagate instantly, and it only covers the folders you move into it. -- Mandatory full-disk encryption protects a lost laptop's *confidentiality*. Nothing protects your data's *existence*. +- The manual's file-safety story is "install Dropbox", which is sync with bounded retention, not backup: it only covers folders you move into it, deleted-file recovery is time-limited, and your data's survival is tied to one account at one vendor. +- Full-disk encryption (the install default) protects a lost laptop's *confidentiality*. Nothing protects your data's *existence*. The ask: paste S3-compatible credentials, and Omarchy handles incremental, versioned, off-site backup from then on. Status, pause, and back-up-now live in a shell panel. Zero ongoing hassle. +## Threat model + +Stated up front so the feature never overpromises: + +- **Defends against**: disk death, machine loss/theft, accidental deletion or overwrite discovered later, a botched migration — anything where you still control your storage account and passphrase. +- **Does not defend against (v1)**: an attacker or malware running as your user on the live machine. The machine necessarily holds credentials that can write to — and, for retention, delete from — the repository, so ransomware with user privileges can attack the backups too. Provider-side object versioning/immutability can mitigate this but is a per-provider, tested procedure (delete markers, prune's space-reclamation assumptions), not a checkbox; the manual documents it honestly. True split-authority (backup-only key on the machine, maintenance from elsewhere) is the v2 path, sketched in open questions. +- **App-consistency limits**: files are read live. Running databases, VM disks, and containers may snapshot in a crash-consistent state at best. The manual says so. +- **Scope boundary**: personal files under `$HOME` for the invoking user. Not `/etc`, not `/var`, not other users, not system package state — the OS is reinstallable from the ISO, and configs are the dots plan's job. + ## Engine choice: restic - **Speaks S3-compatible natively** — AWS, Cloudflare R2, Backblaze B2, Hetzner Object Storage, MinIO, Wasabi — plus SFTP, rest-server, and plain local/USB paths through the same repository URL scheme. One integration, every destination. -- **Incremental without chains.** Content-defined chunking means each run uploads only new data, yet every snapshot is a complete, independently restorable image. No full-vs-incremental chain to replay (duplicity's disease), no periodic re-upload. -- **Versioning is the native model.** Snapshot per run; `forget --keep-hourly/daily/weekly/monthly` retention; `prune` reclaims space. +- **Incremental without chains.** Content-defined chunking means each run uploads only new data, and snapshots have no predecessor chain to replay (duplicity's disease). Snapshots do share deduplicated blobs — integrity of shared data matters, which is why periodic verification is part of the design below. +- **Versioning is the native model.** Snapshot per run; `forget --keep-*` retention; `prune` reclaims space. - **Client-side encryption always on.** The bucket provider stores ciphertext; keys never leave the machine. -- **Multi-machine works out of the box.** Snapshots are tagged with hostname; repository locks serialize maintenance. Two machines can share one bucket with zero coordination. -- Single static Go binary in Arch extra, battle-tested for a decade, `--json` output on every command the panel needs. +- **Multi-machine capable.** Snapshots record hostname; retention groups by host/path; repository locks coordinate concurrent access (with retry handling — see multi-machine section). +- Packaged in Arch extra (`restic`, plus `fuse2` as the optional dependency `mount` needs), battle-tested for a decade, machine-readable `--json` output on the specific commands we script: `backup`, `snapshots`, `stats`, `forget`, `restore`, `version` — enumerated because not every restic command supports JSON. ### Rejected engines @@ -29,51 +38,60 @@ The ask: paste S3-compatible credentials, and Omarchy handles incremental, versi - **borg/borgmatic**: excellent engine, but no S3 backend — needs borg installed on an SSH server, which kills the "paste bucket credentials" UX. - **kopia**: credible restic rival, but ships its own server/UI/scheduler that duplicates the panel and timer we're building anyway, and has far less mindshare. restic is the smaller, better-known dependency. - **duplicity**: incremental chains make restores slow and fragile. -- **Cloud sync clients** (Dropbox, already offered via Install > Service): sync is a different product. Backup must keep what you deleted. +- **Cloud sync clients** (Dropbox, already offered via Install > Service): sync is a different product with different guarantees; backup must keep what you deleted, on your terms, for as long as your retention policy says. ## What gets backed up -`$HOME`, minus a shipped exclude file. Not the system: the OS is reproducible from the installer plus dots; `/home` is the irreplaceable part. +`$HOME`, minus excludes, with `--one-file-system` so mounted NAS shares, removable disks, and FUSE mounts are never traversed by a static pattern list's grace. -- Shipped excludes (`$OMARCHY_PATH/default/backup/excludes`): `~/.cache`, `~/.local/share/Trash`, browser cache dirs, package-manager caches, `node_modules`, thumbnail caches, mounted network shares. Regenerable bytes only — when in doubt, include. +- **Built-in excludes, not user-editable**: the backup's own secrets directory (`~/.local/share/omarchy/backup/` — the repository must never contain the credentials that unlock it), restic's cache (`~/.cache/restic`), `~/.cache`, `~/.local/share/Trash`. +- **Shipped default excludes** (`$OMARCHY_PATH/default/backup/excludes` as `$HOME`-relative patterns, expanded to absolute paths when the setup wizard writes the effective exclude file — restic does not expand `~` in patterns): browser caches, package-manager caches, `node_modules`, thumbnail caches. Regenerable bytes only — when in doubt, include. - The dots repo (`~/.local/share/omarchy/dots.git`) is deliberately *included*: backup is what finally puts the config history off-site. -- Users extend via `~/.config/omarchy/backup/excludes` (one pattern per line, restic syntax). No include-list to curate — that's the hassle we're avoiding. -- The first-run summary shows the measured size before uploading, so a home directory full of VM images isn't a surprise on a metered plan. +- Users extend via `~/.config/omarchy/backup/excludes` (one pattern per line, restic syntax, expanded the same way). No include-list to curate — that's the hassle we're avoiding. +- The first-run summary shows the measured *source* size before uploading — labeled as such, cancellable, and explicitly not an upload estimate (dedup and compression usually shrink it dramatically). ## Setup: `omarchy-setup-backup` -A gum wizard in the terminal, in the mold of `omarchy-setup-security-sshd` (flags for every prompt so it can run unattended), launched from a new _Setup > Backup_ menu entry via `omarchy-launch-floating-terminal-with-presentation`: +A gum wizard in the terminal, in the mold of `omarchy-setup-security-sshd` (flags for every prompt so it can run unattended), launched from a new _Setup > Backup_ menu entry via `omarchy-launch-floating-terminal-with-presentation`. Every step is idempotent and the wizard records its phase, so a closed terminal or dropped connection resumes cleanly on re-run instead of leaving orphaned credentials or a half-initialized repository. Nothing is enabled until the first backup succeeds. -1. **Destination.** Choose: S3-compatible bucket (prompts for endpoint URL, bucket name, access key ID, secret key) / any restic repository URL (expert escape hatch) / local path (USB or NAS mount — free courtesy of restic). -2. **Install + verify.** `omarchy-pkg-add restic`, then probe the repository: if the bucket is empty, `restic init`; if a repository already exists (second machine, or re-setup), ask for its passphrase instead of generating one, and offer the restore path (below). -3. **Encryption passphrase.** Generate a strong passphrase, display it once, and require typed confirmation that it's been saved somewhere off this machine (password manager, paper). Plain warning: lose the passphrase and the backups are unreadable — Omarchy cannot recover it. -4. **First backup runs immediately** in the terminal with live progress, so the user watches it work once. On success: timer enabled, bar widget placed (`enablePlugin` IPC). +1. **Destination.** Choose: S3-compatible bucket — prompts for endpoint URL, bucket (with optional prefix), region (optional, defaulted), access key ID, secret key / any restic repository URL plus extra env vars (expert escape hatch — covers session tokens, custom CAs, path-style quirks) / local path (USB or NAS mount; recorded with its filesystem identity, see below). Unencrypted-disk installs get a plain warning here that the credentials will sit on an unencrypted drive. +2. **Passphrase first.** Generate a strong passphrase (or accept an existing repository's), *then* probe: `restic cat config` distinguishes repository-absent (exit 10 → `restic init`), wrong passphrase (exit 12 → re-prompt), auth/network failures (→ fix credentials, nothing created). Never infer "safe to init" from an apparently empty bucket. +3. **Recovery card.** The wizard writes and displays a recovery record — endpoint, bucket, prefix, repository ID, restore instructions, a blank line for the passphrase — and requires typed confirmation that passphrase and card are saved off this machine. Plain warning: lose the passphrase and the backups are unreadable; Omarchy cannot recover it. The card contains no live credentials. +4. **First backup runs immediately** in the terminal with live progress, so the user watches it work once. On success: timer enabled, bar widget placed (`enablePlugin` IPC). On an existing repository (second machine or reinstall), the wizard detects prior hosts and offers the disaster-recovery restore (below) before any timer starts. -Teardown is `omarchy-setup-backup --remove`: disable timer, remove widget, delete local credentials — after stating clearly that the repository and its snapshots stay untouched in the bucket. +Teardown is `omarchy-setup-backup --remove`: stop and wait for any running backup, unmount any browse session, disable the timer, remove the widget, delete local credentials and state — then state clearly that the repository and its snapshots remain untouched in the bucket, and that restic stays installed. -Credential storage: `~/.config/omarchy/backup/` (0700) holding `env` (`RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) and `passphrase` (0600, referenced via `RESTIC_PASSWORD_FILE`). Plain mode-restricted files are the deliberate choice and the house pattern (agents API keys live in `~/.config/omarchy/agents/`; the gnome-keyring is configured *passwordless* by `install/user/default-keyring.sh`, so `secret-tool` would add a new precedent while buying nothing). Full-disk encryption is mandatory on Omarchy; the passphrase's job is to blind the storage provider, not to survive a compromised user account. A keyring would also break the headless timer — locked keyring at 3am means no backups. +**Secrets layout**: `~/.local/share/omarchy/backup/` (0700) holds `env` (`RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, extras) and `passphrase` (0600, via `RESTIC_PASSWORD_FILE`); secrets pass to restic only via environment and files, never argv. Non-secret settings (destination label, retention, cadence) live separately in `~/.config/omarchy/backup/` — `~/.config/omarchy` is documented user-intent config that people version and that dots will sync, so live delete-capable credentials must not live there. Mode-restricted plain files are still the deliberate choice over the keyring: Omarchy's default keyring is configured passwordless by `install/user/default-keyring.sh` (so `secret-tool` adds a precedent while buying nothing), and a locked keyring at 3am would mean no backups. Full-disk encryption — the default, though not universal — covers the at-rest story; the threat model section covers the rest honestly. ## Automatic runs: systemd user timer -The repo's first `.timer`. `default/systemd/user/omarchy-backup.timer` + `omarchy-backup.service`, shipped to `/usr/lib/systemd/user/` via the `omarchy-settings` PKGBUILD (the `install -Dm644` line must be added there, per `docs/file-layout.md`). Enabled by the setup wizard, not `enable-user-units.sh` — backup is opt-in, so it follows the `omarchy-install-service-tailscale` enable/disable pattern: +The repo's first `.timer`. `default/systemd/user/omarchy-backup.timer` + `omarchy-backup.service`, shipped disabled to `/usr/lib/systemd/user/` via the `omarchy-settings` PKGBUILD in the omarchy-pkgs repo (with the matching `package_defaults` fixture update in `test/shell.d/config-test.sh`). Enabled by the setup wizard, not `enable-user-units.sh` — backup is opt-in, following the `omarchy-install-service-tailscale` enable/disable pattern. -- Timer: hourly `OnUnitActiveSec` with randomized delay, `Persistent=true` so a laptop that slept through a slot catches up on wake, `WantedBy=timers.target`. -- Service: oneshot running `omarchy-backup-run`, with `ConditionPathExists=%h/.config/omarchy/backup/env` (unconfigured = inert) and `ConditionPathExists=!%h/.local/state/omarchy/toggles/backup-paused` (the pause flag gates the unit itself, the `omarchy-crash-watch` idiom). -- `omarchy-backup-run` guards before touching the network: paused → skip; on battery and low (existing `omarchy-battery-*` helpers) → skip; a run already active (lock file, the `omarchy-update.lock` idiom) → skip. Skips are logged as skips, not errors. Missing restic exits 127, the `omarchy-snapshot` convention for "tool not installed". -- Pipeline per run: `restic backup --json` (streamed, see state below), then — on a weekly cadence, not every run — `restic forget --prune` per retention policy and a cheap `restic check`. Retention defaults: `--keep-hourly 24 --keep-daily 7 --keep-weekly 5 --keep-monthly 12`, overridable in a `settings` file the wizard writes. -- Failure policy: a single failed run is silent (flaky café wifi must not nag). No successful backup for 24 hours → one `omarchy-notification-send` warning. The panel always shows the exact state and last error. +- Timer: `OnCalendar=hourly` with `RandomizedDelaySec`, `Persistent=true` — which only works with calendar timers, and gives catch-up-at-login after sleep or shutdown. User timers run only while a session exists; that's fine — backups happen while you use the machine, and `Persistent=` covers the gap. No lingering required. +- Service: oneshot running `omarchy-backup-run`, with `ConditionPathExists=%h/.local/share/omarchy/backup/env` (unconfigured = inert). **Pause is not a unit condition**: a condition-gated unit never executes while paused and so could never notice a pause expiring. Instead the runner always starts, reads the pause record (a resume-at timestamp or `manual` sentinel in the state directory), clears it if expired, and exits as a logged skip if still active. +- Further guards before touching the network: discharging with battery below 20% (a new quiet predicate command — `omarchy-battery-low` is a notifier, not a check) → skip; a run already active (per-machine lock file; restic's repository locks handle cross-machine, below) → skip. Skips are logged as skips and recorded distinctly from failures. Missing restic exits 127, the `omarchy-snapshot` convention for "tool not installed". +- Backup: `restic backup --json` streamed into the state file. Exit 3 (some files unreadable) produces a *partial* snapshot: recorded as `partial`, surfaced in the panel with the unreadable paths, and never advances the last-complete-success marker that restore defaults to. +- Maintenance, tracked separately from backup health: `forget --prune` monthly, not weekly — prune can download and re-upload packs, so it's real bandwidth and API spend. `check` monthly plus a rotating `check --read-data-subset` slice so all data gets read over time — plain `check` alone verifies structure, not content. Retention defaults: `--keep-hourly 24 --keep-daily 7 --keep-weekly 5 --keep-monthly 12`, overridable in settings. +- Notification policy: a single failed run is silent (flaky café wifi must not nag). No successful backup for 24 hours → one `omarchy-notification-send` warning, repeated weekly while the condition persists — one-warning-forever would let a dead credential rot silently for months. The panel always shows the exact state and last error. +- Local-path destinations: setup records the target filesystem's identity (UUID), and every run verifies it before writing — an absent USB disk must produce a skip, not a backup quietly written into an empty mountpoint directory on the root filesystem. Local destinations are labeled non-off-site in the panel. ## Status plumbing: one state file -`omarchy-backup-run` maintains `~/.local/state/omarchy/backup/status.json`: phase (`idle` / `running` / `paused` / `error` / `unconfigured`), progress percent and bytes during a run, last snapshot time and id, snapshot count, repository size, last error text, pause-until timestamp. Written atomically on every phase change, throttled during upload. +`omarchy-backup-run` maintains `~/.local/state/omarchy/backup/status.json`, written atomically (rename) on every phase change and throttled during upload: -This is the agents-plugin pattern (`shell/plugins/agents/`): the background job writes JSON records, the panel is strictly a display watching them with `FileView { watchChanges: true }` (watching the parent directory until the file first exists). The CLI (`omarchy backup status`) reads the same file; nobody shells out to restic for status, so the panel stays instant and the repository is only touched by real runs. +- `phase` (`idle` / `running` / `paused` / `error` / `unconfigured`), `run_id`, `pid`, `started_at`, `updated_at`, progress percent and bytes during a run +- `last_backup` { time, snapshot id, result `complete` / `partial` / `failed`, error text, unreadable paths } +- `last_maintenance` { time, result, error } — separate, so a prune failure doesn't masquerade as a backup failure +- `snapshots` (recent list for the panel), `destination` (redacted label — provider/bucket, never credentials), repository size, snapshot count, `pause` { until, reason } +- Stale-run reconciliation: on every start, the runner checks a `running` record's pid; a dead pid (crash, reboot mid-backup) is rewritten as `failed` so the panel can never show a phantom eternal run. + +This is the agents-plugin pattern (`shell/plugins/agents/`): the background job writes JSON, the panel is strictly a display watching it with `FileView { watchChanges: true }` — keeping the parent-directory watcher permanently and re-binding after the atomic rename, since `FileView` loses files that are replaced rather than modified. The CLI (`omarchy backup status`) reads the same file; nobody shells out to restic for status, so the panel stays instant and the repository is only touched by real runs. ## Panel: `omarchy.backup` bar widget A first-party plugin at `shell/plugins/panels/backup/`, modeled on the dropbox panel — the closest existing analog (background daemon with pause/resume, status, quota): `manifest.json` (kinds `["bar-widget"]`), `Panel.qml` extending `Ui/Panel.qml`, `Service.qml` for state, `Model.js` for parsing, built from the shared `PanelHero` / `PanelSectionHeader` / `PanelActionButton` kit with `Color`/`Style` theme tokens. -- **Bar icon states**: unobtrusive when healthy, progress animation while a run uploads, dimmed when paused, attention color when the last run failed or no backup has succeeded in over a day. +- **Bar icon states**: unobtrusive when healthy, progress animation while a run uploads, dimmed when paused, attention color when the last run failed, was partial, or no complete backup has succeeded in over a day. - **Panel**: hero line ("Backed up 12 minutes ago to Backblaze"), repo size and snapshot count, live progress bar during a run, recent snapshots list, and actions: **Back Up Now**, **Pause** (1 hour / until tomorrow / until resumed) / **Resume**, and a link into the restore workflow (opens a terminal — restores confirm and print, which wants a terminal, same rationale as the plugin Add/Remove flows). - Actions call `omarchy backup now|pause|resume` detached, with the dropbox panel's optimistic-state trick (`_desired` overrides reality until the state file catches up) so buttons react instantly. - IPC target `omarchy.backup` (`manageIpc: false`, custom verbs `refresh` / `run` / `status` on top of the inherited open/close/toggle), reachable as `omarchy-shell omarchy.backup toggle`. @@ -84,31 +102,32 @@ A first-party plugin at `shell/plugins/panels/backup/`, modeled on the dropbox p `GROUP_DESCRIPTIONS[backup]` added in `bin/omarchy`; the router derives verbs from filenames, so these are just `bin/omarchy-backup-*` executables with `# omarchy:summary=` metadata: - `status` (`--json` for scripts), `now`, `pause [duration]`, `resume`, `log`, `snapshots` -- `restore [--at ]` — restores next to the original as `.restored` by default; `--overwrite` restores in place after a confirm. Accepts a directory to recover a whole tree. -- `browse` — `restic mount` on a runtime-dir FUSE mount + opens the file manager: every snapshot browsable as dated folders. This is the "super easy" restore story — no flags to learn, just copy files out. -- **Disaster recovery**: on a fresh machine, `omarchy-setup-backup` pointed at the existing bucket asks for the passphrase, detects prior hosts in the repository, and offers "restore home folder from 's latest snapshot" before the timer even starts. This is the payoff of the whole feature and belongs in v1. +- `restore [--at ]` — restores into a staging directory (`~/Restored//`, preserving relative layout) by default; `--in-place` overwrites after a confirm and after snapshotting the current version first, so an interrupted in-place restore is itself recoverable. Paths are passed to restic as literal include arguments, never shell-interpreted. Defaults to the last *complete* snapshot, never a partial one. Accepts a directory to recover a whole tree. +- `browse` — `restic mount` (which needs `fuse2`, installed by setup alongside restic) run as a foreground process in a terminal on a runtime-dir mountpoint, with the file manager opened on it; unmount on exit, with stale-mount cleanup at the start of every run and at teardown. Every snapshot browsable as dated folders — the "super easy" restore story: no flags to learn, just copy files out. +- **Disaster recovery**: on a fresh machine, `omarchy-setup-backup` pointed at the existing bucket asks for the passphrase, lists prior hosts, and offers "restore home folder from 's latest complete snapshot" into the new home before the timer starts — mapping the snapshot's old home path onto the new one (restic restores the recorded hierarchy; the wizard handles the `snapshot:subfolder` mapping and warns when the username differs). This is the payoff of the whole feature and belongs in v1. -## Versioning and conflicts +## Versioning, conflicts, and multi-machine Explicitly part of the design, and deliberately boring: -- **Versioning** is restic's snapshot model plus the retention policy. Nothing is ever overwritten server-side; a version disappears only when retention forgets it. -- **Conflicts cannot happen** because backup is one-way and append-only. Machines never merge state: each run adds a snapshot tagged with its hostname, restic's repository locks serialize concurrent maintenance, and restores default to your own host's snapshots. Two-way file sync is a different product (Dropbox for files, dots for configs); this feature refuses to become one — that refusal *is* the conflict story. -- **Honest limit**: credentials on the machine can delete the bucket, so ransomware running as the user could destroy the backups it's supposed to protect against. v1 documents the mitigation (bucket-side object lock / versioning, or a second restricted key) in the manual; an append-only mode is an open question below. +- **Versioning** is restic's snapshot model plus the retention policy. Nothing is overwritten server-side; a version disappears only when retention forgets it. +- **Conflicts cannot happen** because backup is one-way and append-only in its semantics. Machines never merge state: each run adds a snapshot recording its hostname, retention groups by host, and restores default to your own host's snapshots. Two-way file sync is a different product (Dropbox for files, dots for configs); this feature refuses to become one — that refusal *is* the conflict story. +- **Coordination is real, though**: restic's repository locks default to zero retries, so runs use `--retry-lock` and treat lock contention (exit 11) as a retryable skip, not a failure. Exactly one machine — the first one set up, recorded in the repository — runs prune and check; the others only back up, which keeps the expensive, contention-prone maintenance in one place. Hostname is the retention grouping key, so the manual notes that cloned machines need distinct hostnames. +- **Shared-repository trade-off, stated honestly**: all passphrases on a shared repository unlock the same master key, so compromising one machine exposes every machine's data in it, and removing a password later doesn't un-ring that bell. The default stays one shared repository — it's what makes cross-host disaster recovery a first-run feature — with per-machine repositories documented for people who want isolation over convenience. ## Rollout -- Fully opt-in: nothing installed, no timer, no widget until _Setup > Backup_ runs. +- Fully opt-in: no packages installed, no units enabled, no scheduled work, no widget until _Setup > Backup_ runs (the unit files themselves ship disabled with `omarchy-settings`, like every other default unit). - No migration needed for existing users. -- Docs per the documentation layout: a new `manual/49-backups.md` (user guide: setup, restore, browse, the passphrase warning, multi-machine), `docs/backup.md` (reference: state file schema, unit wiring, retention), and `manual/47-system-snapshots.md` / `24-commercial-apps-services.md` cross-links so "snapshots vs backup vs sync" is stated in one place. -- Tests: restic's local backend makes the whole engine testable hermetically — no S3 in CI. Cover: CLI metadata (`omarchy commands --check` via `test/cli` fails without summaries); state-file transitions (running → idle, error paths); pause flag honored by both the unit condition and `omarchy-backup-run`; excludes actually applied; retention args; restore round-trip (backup a fixture tree to a local repo, restore, diff); setup wizard's unattended flag path; menu acceptance test for the new entry. +- Docs per the documentation layout: a new `manual/49-backups.md` (user guide: setup, restore, browse, the passphrase warning and recovery card, multi-machine, the threat model in plain words — including provider lifecycle policies: cold-storage/archival tiers silently break restic and must not be enabled on the bucket), `docs/backup.md` (reference: state file schema, unit wiring, retention, lock/maintenance topology), and `manual/47-system-snapshots.md` / `24-commercial-apps-services.md` cross-links so "snapshots vs backup vs sync" is stated in one place. +- Tests: restic's local backend covers the engine logic hermetically — CLI metadata (`omarchy commands --check` via `test/cli`), state-file transitions including stale-run reconciliation and partial results, pause expiry, exclude expansion (tilde-free, absolute), retention args, restore round-trip into staging (fixture tree → local repo → restore → diff), wizard's unattended flag path and resume-after-abort, menu acceptance. S3 specifics (endpoint styles, auth failures, prefixes) get an integration pass against MinIO in the acceptance VM. The widget and panel go through `agents/skills/visual-verification.md` in the running UI: unconfigured, first backup, running, partial, failure, stale, paused, browse-mounted, long labels, teardown. ## Open questions 1. Default excludes: do multi-GB regenerables like VM disks and local LLM models stay in (include-by-default purity) or out (metered-connection mercy)? The first-run size preview softens either answer. -2. Cadence and retention defaults: hourly may be too chatty for metered/mobile connections — detect metered via NetworkManager and skip, or make cadence a wizard question? -3. Append-only protection: rest-server has `--append-only`, S3 needs provider object-lock. Worth a guided option in the wizard, or documentation only? +2. Cadence: hourly may be too chatty for metered/mobile connections — detect metered via NetworkManager and skip, or make cadence a wizard question? +3. Split-authority hardening (v2 sketch): a backup-only credential on the machine (no delete permission), with prune run by the designated maintenance machine under a separate credential — or provider-side object versioning with a tested recovery runbook per provider. Which providers make this tractable enough to wizard? 4. Bandwidth limiting (`--limit-upload`): expose in settings, in the wizard, or not at all in v1? 5. Should the bar widget exist for unconfigured users as a discoverable "Backups: not set up" nudge, or is the menu entry enough? (Leaning: menu only; the bar is not for ads.) 6. Naming: plain `omarchy backup` vs a branded name in the dots tradition. Plain reads better in a disaster ("omarchy backup restore") — leaning plain. -7. Timed pause ("until tomorrow") needs more than a touch file: store the resume timestamp in the flag and have the next timer firing clear an expired pause, or lean on `systemctl --user` transient timers? +7. Per-machine repositories as a wizard option (key isolation) vs documentation only, given the shared-repo default exists for cross-host restore.