Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Backup Mirror (encrypted off-machine snapshot) (part 2)

The second half of the Backup Mirror page: first-time activation, the automatic-sync modes, the encryption envelope, the archive's internal layout, retention, the restore safety defences and the IPC channels.

What it is

This is part 2 of the Backup Mirror (encrypted off-machine snapshot) page. It carries first-time activation, the automatic-sync modes, the encryption envelope the archives use, the archive's internal storage layout, retention, the defences the restore path applies to an untrusted archive, and the IPC channels the settings panel drives.

Where to find it

Backup Mirror is configured at Settings → Backup & Restore → Backup Mirror, where the folder, passphrase, retention and Enable toggle live and where the mirror and restore cards report status; Backup Mirror (encrypted off-machine snapshot) walks that panel end to end, including the two restore paths and the two-device Sync now flow. The reference tables and mechanics on this page are read from the same panel or from the code paths named below.

How it behaves

Activation (first-time setup)

Three small conveniences make first-time setup near-one-click. They are pure UX around the same encrypted write/restore core — they change nothing about the crypto or the no-escrow model:

  • Cloud-folder detection. Omniscio probes for existing Dropbox / OneDrive / iCloud Drive / Google Drive roots (reading the %OneDrive% env var and Dropbox's own info.json, so a relocated install is still found) and offers each as a one-click chip that targets <root>/Omniscio-mirror. Read-only + best-effort — an empty result just means "Browse manually" — and the subfolder is created on the first write (its parent, the detected root, already exists). Channel: backup-mirror:detect-cloud-folders.
  • Passphrase generator. Generate strong passphrase produces a diceware-style, typeable passphrase (≈56 bits) in the app window — it never crosses the IPC boundary. A mandatory I've saved this passphrase checkbox gates the first write, scoped to genuine first-time setup (lastBackupMirrorAt empty) so it never blocks an already-working mirror.
  • Auto-sync default. Enabling the mirror for the first time sets Automatic sync to session (see Automatic sync) — only at first setup, and never overriding a returning user's deliberate Off.

Implementation: src/shared/backup-mirror-activation.ts (generator + the enable predicate), src/main/services/backup/detect-cloud-folders.ts (detector). Locked by the Activation invariants in backup-mirror-contract.md.

Automatic sync (hands-off, two devices)

Sync now is one click — but you can also have Omniscio do it on its own. Settings → Backup & Restore → Backup Mirror → Automatic sync is a dropdown with three choices (backupMirrorAutoSyncMode):

  • Off — nothing automatic; the manual Sync now / Mirror now buttons are the only path. (off is the stored default, but the FIRST time you enable the mirror Omniscio bumps you to session below — only at that genuine first setup, and never overriding a returning user who set it back to Off. See Activation defaults.)
  • When I open & close Omniscio (session) — Omniscio does a pull-then-push when it launches (receives the other device's latest and sends yours) and a fast pull-only catch-up when you quit. Once per session — the sane cadence for a snapshot of ~1–3 GB before compression, and enough to keep two devices in step. (The send happens at launch, not quit, so a big write can never hang the app while it closes — see below.)
  • On a schedule (scheduled) — the same pull-then-push on a coarse timer you pick: every 1 / 3 / 6 / 12 hours (backupMirrorAutoSyncIntervalMinutes). Sub-hourly is intentionally not offered — each snapshot can be 1–3 GB before compression.

It never rewrites a big archive for nothing. Two cheap checks gate the heavy work:

  • Pull short-circuit — a stat-only look at the folder; if no archive newer than the one it last pulled (lastAutoSyncPulledMirrorId) has appeared, it skips the decrypt entirely.
  • Push short-circuit — a cheap fingerprint of your database (newest message row + time, newest session row, lastPushedMirrorChangeMarker). If nothing changed since the last mirror, the push is skipped — so "open Omniscio, glance at it, close it" writes nothing. The marker errs toward pushing, never toward skipping a real change. The mirror that rides a local backup uses the SAME fingerprint, and stamps it on success, so a backup round that carried no new data costs no write and the next scheduled push does not re-encrypt a bundle that is already current.

The push is deliberately always preceded by a pull (never push-only), because the retention prune sweeps the whole folder newest-first — pulling first guarantees the other device's archive is merged before any prune could remove it. The push runs at LAUNCH and on the scheduled timer, NEVER on the shutdown path (F1): writeBackupMirror is a multi-GB snapshot + streamed encrypt-and-write to a user-chosen (often cloud) folder — so a shutdown push could hang app quit past the 15s safety net (and an offline cloud folder would block forever). It streams now (memory-bounded, non-blocking main thread), which removes the freeze risk, but it stays off the quit path because the write itself can still take a long time or block on an offline mount. Quitting does a fast pull-only; this device's work propagates on the next launch's push. Also: the pull only advances its "already seen" marker when the folder was fully readable, so a foreign archive still uploading is re-checked next time instead of skipped forever.

If an automatic push of a fully-configured mirror can't complete — the folder vanished, the disk is full, a write errored — Omniscio raises a persistent "Backup Mirror sync is failing" card in the inbox so a broken off-site mirror isn't silently buried in the settings panel. It's reconciled on every push and backfilled when Omniscio opens (so a failure from last shutdown isn't missed), and it clears itself the moment a push succeeds again.

If the mirror is enabled but not finished — you flipped the toggle on but haven't set a passphrase (or picked a folder) yet — Omniscio shows a separate, gentle "Finish setting up Backup Mirror" reminder (tailored to what's missing) instead of the alarming failure card. Nothing is backed up off-site until setup is complete, so the reminder is the honest signal — not a runtime error about a mirror that never ran. It appears the moment you enable an unconfigured mirror, is re-checked at every launch (regardless of auto-sync mode), and clears itself the instant you finish setup or turn the mirror off. This is why turning the mirror on without a passphrase no longer produces a misleading "sync is failing" notice. See backup-failure-alert-contract.md.

Status you can see. When automatic sync is on, the Settings panel shows Last synced … plus, when relevant, "New work available from your other device" (a newer foreign archive exists) or "This device has changes to send" (your DB changed since the last push). Failures surface as a plain message rather than failing silently.

What it is not. Not live/continuous sync — it fires at launch, at quit, or on the timer, and relies on your cloud-sync client to move the files between devices. A brand-new device still needs the folder + passphrase entered once (the passphrase can't be auto-derived without defeating the encryption); after that one-time pairing it's hands-off.

Behind the scenes: src/main/services/backup/backup-mirror-auto-sync.ts — runLaunchPull() → runAutoSync() (launch: pull and push), runAutoSync({ push: false }) inside gracefulShutdown() (shutdown: pull-only, before the early config flush), refreshAutoSyncScheduler() (the scheduled timer: pull and push). The launch pull+push is a deferred startup task that runs after the window is shown, so a slow write is never on the quit critical path; the timer is (re)armed live when you change the mode, the interval, or the Enable toggle. All three modes additionally require the master toggle plus a folder and a passphrase (isBackupMirrorFullyConfigured) — turning the mirror off stops the scheduled push and disarms the timer immediately, not at the next restart.

Encryption

Same key derivation and cipher as Setup Backup to Gmail: AES-256-GCM with a PBKDF2-SHA256 key at 600,000 iterations. The salt and the base nonce are random per archive, so two mirrors of identical state produce different ciphertexts. The file is a stream of frames rather than one sealed blob: a fixed header (the marker AMCSTRM1, a stream version, the key-derivation mode, the salt, the base nonce and the frame size), then frames of up to 1 MiB, each carrying its own length, a final-frame flag, its ciphertext and its own 16-byte AES-GCM tag. The header is bound into every frame's authentication and each frame's nonce comes from its position plus the final-frame flag, so a reordered, duplicated, dropped or truncated frame fails decryption just like a tampered byte.

Compression happens before encryption. The whole archive is compressed with zstd first and only then split into frames — never frame by frame, because a frame's ciphertext is capped at 1 MiB and an incompressible chunk grows slightly. Ciphertext cannot be compressed, so this is the only point where the file can shrink, and a database-heavy mirror ends up much smaller than the state it holds. This is stream version 2, the version written now. Mirrors written before compression (stream version 1: the same frames with no compression step) still restore — the reader takes the version from the header and decompresses only version 2 — and so do the oldest whole-buffer archives, whose envelope is [salt(16) | iv(12) | ciphertext(N) | authTag(16)].

Wrong passphrase or any tampered byte fails decryption — there is no partial recovery, no oracle, no graceful degradation.

The passphrase is stored locally in config.json under backupMirrorPassphrase, safeStorage-encrypted with the enc: prefix (same scheme as OAuth tokens). It is never written into the archive and never crosses the IPC boundary outward — backup-mirror:get-status returns only hasPassphrase: boolean.

Implementation: src/main/services/backup/mirror-stream-crypto.ts (the stream envelope and its compression step, shared with Portable Backup) and src/main/services/backup/setup-backup-crypto.ts (the key derivation shared with Setup Backup, and the older whole-buffer envelope).

Storage layout

Each archive lives at <mirrorPath>/mirror-<id>.amcmirror where <id> is the ISO timestamp at write time with : and . replaced by - (so it's filesystem-safe and time-orderable). The archive's internal layout, once decrypted and decompressed:

manifest.json          // schema/version/byte-counts; first thing the restore reads
mission-control.db     // VACUUM'd snapshot
config.json            // settings + prefs (provider/integration secrets stripped or machine-bound — re-enter on a new machine)
attachments/           // entire tree, preserved with subdirectory structure
  …
claude-code/           // Claude Code files from ~/.claude/ (optional)
  CLAUDE.md            // global rules
  projects/
    <project-key>/
      CLAUDE.md        // per-project instructions (if present)
      memory/          // per-project auto-memory
        …
skills/                // user-authored ~/.claude/skills/ (optional)
  <skill-name>/        // one dir per skill — SKILL.md + any scripts/assets
    …
global-claude-config/  // ~/.claude/ config allowlist (optional; never secrets/creds)
  settings.json        // permissions, hooks, model/statusline prefs
  keybindings.json     // custom shortcuts (if present)
  scripts/             // deployed automation scripts (if present)
    …
recipes/               // ~/.claude/recipes/*.recipe.json definitions (optional)
  <name>.recipe.json   // file-based recipe defs (DB rows travel separately)
vault/                 // KMS/Nothari vault: notes + assets (optional)
  <note>.md            // user-authored notes (any subdirectory depth)
  assets/              // image store the notes reference
    …
project-notes/         // per-project free-text notes (optional)
  <projectId>.md       // one file per project
books/                 // user-imported Books-plugin books (always, when any exist)
  <bookId>/            // one dir per book — EPUB/PDF + cover
    …
google-workspace-mcp/  // google-workspace MCP audit store (when configured)
  workspace.db         // audit_log + batch history — the DB only, never credentials/
arij-attachments/      // Arij issue attachments (optional)
  <issueId>/           // one subfolder per issue
screenshots/           // captured screenshots (always, when any exist)
  <uuid>.png           // + annotation sidecars
screen-recordings/     // screen recordings (opt-in — backupMirrorIncludeScreenRecordings)
  …                    // MP4s, segments, thumbnails (excludes the screenshots subdir)

Writes are atomic — Omniscio writes to mirror-<id>.amcmirror.tmp first, then renameSyncs to the final name. Cloud-sync clients can otherwise pick up a partially-written file and replicate the corruption.

Retention

After every successful write, Omniscio reads the mirror folder, sorts archives newest-first, and unlinks anything beyond the configured count (backupMirrorRetentionCount, default 5). Only files matching mirror-*.amcmirror are touched — random files you've dropped in that folder are left alone. The retention sweep is best-effort: failure to delete an old file is logged but does not fail the mirror write.

Cross-device-safe. When two devices share the folder, the prune keeps the newest retentionCount archives folder-wide and the newest archive of every other device, so one device can never delete another device's latest snapshot before it has been pulled and merged. Each archive's filename encodes the writing device (its id is <time>.<hostslug>.<random>), so the prune tells devices apart without decrypting; the random suffix also stops two devices writing in the same millisecond from colliding on one filename. A single-device user sees identical behaviour (its own archives still prune folder-wide to retentionCount).

Restore safety

The decrypt path defends against more than honest mistakes — a passphrase-encrypted archive still arrives over an untrusted folder (a Dropbox share, a USB stick, a network drive a coworker has access to). Defenses:

  • AEAD failure means hard stop. A wrong passphrase or tampered byte fails GCM authentication; the restore throws Failed to decrypt mirror — wrong passphrase or tampered file before any byte is staged.
  • Manifest validation. A missing or malformed manifest.json, a manifestVersion other than 1 or 2, or a missing required field throws before any DB write. Unsupported versions get an explicit message so the user knows whether to upgrade Omniscio or use a different archive.
  • Manifest id charset guard. The archive's self-reported id is validated to the writer's charset ([A-Za-z0-9._-], no ..) before it can be interpolated into a temp file path on the merge path — an unvalidated id could otherwise steer a write out of the userData dir. Passphrase-gated (the archive must decrypt first), so this is defense-in-depth.
  • DB-missing-from-archive. An archive without mission-control.db (e.g. a partial or corrupt ZIP) throws and cleans up any staging dir that the staging-phase touched.
  • Schema mismatch (merge only). The schema-version gate above. Replace mode is allowed to cross schemas because the on-disk DB carries its own version and Omniscio's startup migrations will upgrade it.
  • Pre-restore snapshots are mandatory (replace only). If the snapshot copy fails (disk full, I/O error, permission denied), the restore aborts before staging any files. The user's current state is preserved. There is no "skip the snapshot and restore anyway" fallback.
  • Atomic swap. The swap step is the last thing to run. If a crash occurs between staging and swap, the live state is still intact and a restart resumes the swap. If a crash occurs during the swap (rare — renameSync is atomic), the next startup completes whatever steps remained.

For agents

IPC channels

Channel Direction Purpose
backup-mirror:get-status renderer → main Fetch the panel state — enabled, hasPassphrase (boolean projection), mirrorPath, lastBackupMirrorAt, lastBackupMirrorError, retentionCount, plus the auto-sync fields autoSyncMode, autoSyncIntervalMinutes, lastAutoSyncAt, remoteHasNewer, localHasUnsentChanges. The plaintext passphrase never crosses outward.
backup-mirror:run-now renderer → main Force an immediate mirror write (synchronous — the IPC path has no request deadline, so it returns the manifest payload for the panel toast). On success writes lastBackupMirrorAt, clears lastBackupMirrorError, and clears the backup-mirror-failing inbox card. On failure records lastBackupMirrorError, raises that inbox card, and rethrows so the panel surfaces the message. (The twin CLI route POST /backup-mirror/run-now instead runs the write in the BACKGROUND and returns 202 at once — see the contract.)
backup-mirror:sync-now renderer → main One-button Sync now: pull the newest other-device archive (merge) then push a fresh snapshot of this device. Pull-then-push so the retention prune can't drop the foreign archive first. Reads the stored passphrase in main (never over IPC); returns per-half { pull, push } outcomes so a failed push never hides a merged pull.
backup-mirror:list renderer → main List .amcmirror files in the configured folder — stats-only (id, absolutePath, sizeBytes, modifiedAt). Does NOT decrypt or open any archive. Returns an empty array when backupMirrorPath is unset.
backup-mirror:pick-folder renderer → main Open an OS directory picker with createDirectory permission for the "Pick folder" button. Returns { folderPath: string | null } — null when the user cancels.
backup-mirror:restore renderer → main Restore from a previously-written archive. Mode = replace or merge-conversations. Replace stages files for next-startup swap and returns requiresRestart=true; merge applies directly to the live DB.
backup-mirror:browse-preview renderer → main Open an OS file picker filtered for .amcmirror, decrypt the manifest, mint a provenance ticket. Returns preview data (manifest summary, sameHostname flag) + a single-use ticket. Returns error 'cancelled' when the user dismisses the picker.
backup-mirror:browse-execute renderer → main Redeem a provenance ticket from browse-preview and execute restoreFromMirror. The renderer never names a filesystem path — the ticket maps back to the path the user picked. A stale/replayed ticket returns an error envelope.

Handlers live in src/main/ipc/backup-mirror-handlers.ts; the writer in src/main/services/backup/backup-mirror-service.ts; the restore in src/main/services/backup/backup-mirror-restore-service.ts.

Related

This page is the companion to Backup Mirror (encrypted off-machine snapshot), which covers the feature itself: what gets backed up, when it writes, the restore flow and resume after restore. Setup Backup to Gmail is the lighter, configuration-only sibling that shares this page's crypto envelope, Portable Backup is the export that escrows a recovery code instead, and tray-and-window.md covers the graceful-shutdown sequencing the pending-restore swap depends on.

Last verified 2026-10-06