---
title: Setup Backup to Gmail
---
# Setup Backup to Gmail

## What it is

Weekly encrypted backup of your Omniscio **configuration** (not your conversations) emailed to your own Gmail inbox, with a one-click restore that pulls a bundle back out of email.

Omniscio builds a small ZIP every seven days holding everything you'd need to rebuild your Omniscio setup on a fresh machine — the row data from 18 SQLite tables (projects, bookmarks, snippets, automations, recipes' metadata, away-mode rules, etc.), your non-secret app settings, your `~/.claude/recipes/` files, and any user skill folders under `~/.claude/skills/`. The ZIP is encrypted with a passphrase **only you know**, then sent as an attachment to your own primary Gmail address with the label `Omniscio Backup`.

The point is recovery, not history: if your laptop dies, your `config.json` is corrupted, or you want to mirror your Omniscio layout onto a second machine, the most recent email in `Omniscio Backup` plus your passphrase get you back to a working state in one round-trip.

**It is not a chat backup.** The bundle never contains your conversation messages, session transcripts, attachments, the SQLite database file itself, your Claude OAuth tokens, your API keys, or any other secret that lives in `config.json`. See [What gets backed up](#what-gets-backed-up) below for the full inclusion list and the deliberate exclusions.

### What gets backed up

**Included** (per `src/main/services/backup/setup-backup-service.ts` `SETUP_TABLES` + the `config/` and `recipes/` and `skills/` zones):

- **18 SQLite tables** (alphabetical): `automations`, `away_mode_rules`, `bookmarks`, `cron_jobs`, `deploy_profile_entries`, `deploy_profiles`, `email_summarizer_rules`, `project_dividers`, `projects`, `prompt_sequences`, `quick_replies`, `recipe_schedules`, `rss_feeds`, `saved_prompts`, `spam_rules`, `tag_project_scopes`, `tags`, `webhook_sources`. These are the tables that describe your _setup_ — what projects you've added, what bookmarks you've saved, what snippets you've authored, etc.
- **Non-secret app settings** from `config.json` — theme, keyboard shortcuts, sidebar order, feature toggles, and so on. Filtered through `src/main/services/backup/setup-backup-service.ts`, which deletes a denylist of known secret keys (OAuth tokens, API keys, `setupBackupPassphrase` itself, `lastActiveProjectId`, `lastActiveSessionId`). The separate recursive `src/main/services/backup/setup-backup-service.ts` walk — which nulls any string starting with `enc:` (the `safeStorage`-encrypted prefix used for credentials) — runs on the bundle's `preferences` payload (SSH remotes, muted projects, silence-until), not on the settings payload.
- **Recipe files** — every `.recipe.json` under `~/.claude/recipes/` (your global recipes directory).
- **User skills** — every folder under `~/.claude/skills/` containing a `SKILL.md`. Each skill is capped at 5 MB, with a 15 MB total cap across all skills, so a single oversized skill cannot bloat the bundle.

**NOT included** — these are deliberate exclusions, not bugs:

- **Conversation messages and session history.** The `sessions` and `conversation_messages` tables are excluded entirely. A restored Omniscio has the same project/bookmark layout but starts fresh — your old chats stay on the old machine.
- **Attachments.** Anything you've pasted, dragged, or paperclipped into a session.
- **The SQLite DB file itself.** Omniscio reconstructs each table from JSON, not from a copy of `mission-control.db`. This keeps the bundle small and lets you restore across schema versions safely.
- **Claude OAuth tokens, API keys, refresh tokens, and any other `safeStorage`-encrypted credential.** In the settings payload these are removed by the secret-key denylist; in the `preferences` payload any `enc:...` string is nulled by the recursive redaction walk. After a restore you re-sign in to Claude / Gmail / etc. once.
- **Your encryption passphrase.** The passphrase is stripped from the bundle. It lives in your local `config.json` (safeStorage-encrypted with the `enc:` prefix — same protection Omniscio uses for OAuth tokens and API keys), but never travels to Gmail. Forget the passphrase and your encrypted bundles become unreadable; Omniscio has no recovery path. If you need a backup you can still open after losing the passphrase, use [Portable Backup](portable-backup.md) instead — it escrows a one-time **recovery code** that unlocks the archive on any machine even without the passphrase.

## Where to find it

Open **Settings → Backup & Restore → Setup Backup to Gmail** (or type "setup backup" into the settings search bar). Restore lives in the same panel: point it at the `.amc-backup` file you downloaded from Gmail, type the passphrase, and choose Replace or Merge.

## How it behaves

### How to use it

1. Open **Settings → Backup & Restore → Setup Backup to Gmail** (or just type "setup backup" into the settings search bar).
2. Connect your Gmail account if you haven't already (the same one-click Google OAuth that powers Calendar / Drive / Sheets / Gmail integration).
3. Pick a passphrase, type it into the **Encryption passphrase** field, and click somewhere else — it saves on blur, not on every keystroke. Stash this passphrase in your password manager. **Without it there is no encryption at all** — see the note below — and with it, a lost passphrase means the bundle is unrecoverable (there is no escrow and no recovery code).
4. Toggle **Enable weekly backup** on. Omniscio will run a backup five minutes after the next app launch and then once every seven days, silently in the background.

> **Set the passphrase BEFORE you enable the weekly schedule.** Encryption is conditional on that field being non-empty: the scheduler checks it at send time and, when it is empty, emails the bundle as a **plain, unencrypted ZIP** — the passphrase ships blank by default and the weekly toggle does not require one. The status card's **Encryption** row reads "Not set" while that is the case, so check it there if you are unsure.
5. Click **Back up now** if you want to verify the round-trip end-to-end before trusting the schedule. The status card updates with `Last backup: just now` and the bundle lands in your `Omniscio Backup` Gmail label within a few seconds.

To restore, click **Restore from backup** in the same panel. A dialog opens, you point it at the `.amc-backup` file you just downloaded from Gmail, type the passphrase, and pick **Replace** or **Merge** mode. The dialog shows a preview (hostname, schema version, row counts) before you commit.

Omniscio stops every running session before it replaces the database, on **whichever engine runs it** — Claude's and every external engine's — because replacing the database underneath a live agent is how work gets lost. If any agent will not stop, the restore is **refused** before anything is replaced, with *"Some agents did not stop, so nothing was changed. Stop them and try again."* Stop those sessions by hand (see [pause or stop a session](pause-or-stop-a-session.md)) and restore again. A refused restore costs you a click; a restore taken under a live writer cannot be undone.

### Encryption

The bundle is encrypted client-side, in the Omniscio main process, **before** it leaves your machine. The crypto is a portable AEAD with no OS keyring dependency, which is what lets restore work on a different computer.

- **Algorithm**: AES-256-GCM (authenticated, so a wrong passphrase or any tampered byte fails decryption — you never get partial recovery).
- **KDF**: PBKDF2 with SHA-256, **600,000 iterations** (matches the OWASP 2023 minimum). Slow on purpose — typical key derivation takes a few hundred milliseconds, which is fine for a once-a-week backup but expensive enough to defeat brute-force on a leaked bundle.
- **Envelope layout** (concatenated bytes, no magic header): `[salt(16) | iv(12) | ciphertext(N) | authTag(16)]`. Total overhead 44 bytes.
- **Salt and IV are random per backup**, so two backups of identical data produce different ciphertexts — there's no oracle for "did your config change this week?".
- **Detection on restore**: the file is a ZIP if its first four bytes are `PK\x03\x04`, otherwise it's treated as encrypted and `decryptBundle()` is run. So plaintext ZIPs (debug exports) and encrypted bundles share the same `.amc-backup` extension and the restore dialog handles both.
- **Forward compatibility note.** The on-disk envelope has no magic bytes or version field, so any future change to encryption parameters will be a flag-day migration — old bundles won't be distinguishable from new ones by inspection alone, and would need a new file extension or wrapper format to coexist.

Implementation: `src/main/services/backup/setup-backup-crypto.ts`.

### Email delivery

Email delivery uses a small separately-installed Google Workspace CLI. Omniscio ships with two backends and you pick one at **Settings → Gmail → Google CLI Backend**:

- **gog** (current default) — the `openclaw/gogcli` Go binary that's installed and authenticated on this machine today. Build from source per the [openclaw/gogcli README](https://github.com/openclaw/gogcli), drop the resulting binary into `~/.local/bin/`, and run `gog auth add <you@example.com>`. This is the backend backups use unless you switch.
- **gws** (official, opt-in) — the official Google CLI at `github.com/googleworkspace/cli`. Install on Windows with `winget install Google.WorkspaceCLI`, on macOS/Linux with `brew install googleworkspace-cli`, or on any platform with `npm install -g @googleworkspace/cli`. Omniscio's Tools view also offers a one-click install. gws keeps its **own** Google credentials — it can't reuse gog's — so after install you must run `gws auth login` once before flipping the backend setting to **gws**.

Omniscio does not bundle either CLI; if the selected backend isn't on your PATH, the backup fails with a "command not found" error in the failure banner. Sending uses the same OAuth scope you already granted for that backend — no new permissions are requested.

- **Recipient**: your primary Gmail address (resolved via `gog me` / `gws gmail users getProfile`, picking the entry flagged as both `primary` and `sourcePrimary`, with verified-email fallback).
- **Subject**: `Omniscio Setup Backup — <hostname> — YYYY-MM-DD`.
- **Body**: a short reminder that the bundle is encrypted and recovery requires your passphrase.
- **Attachment**: `setup-backup-<timestamp>.amc-backup` (the encrypted bytes).
- **Label**: `Omniscio Backup` (created on first send, then reapplied on every send so the inbox can be filtered down to backups easily).
- **Size cap**: Gmail itself rejects attachments over 25 MB. Omniscio enforces a 20 MB cap on the **plaintext** ZIP before encryption (encryption adds only 44 bytes), so the email always fits with margin. If your bundle would exceed 20 MB you'll see a failure card in the panel telling you which sub-cap you blew through (`PER_SKILL_CAP_BYTES`, `TOTAL_SKILLS_CAP_BYTES`, or the global cap).

Implementation: `src/main/services/backup/setup-backup-mailer.ts`.

### Schedule

- **Interval**: 7 days (`BACKUP_INTERVAL_MS` in `src/main/services/backup/setup-backup-scheduler.ts`).
- **Startup delay**: 5 minutes after app launch — gives your other services time to settle so the first tick doesn't compete with onboarding.
- **Tick frequency**: every 1 hour the scheduler wakes up and asks "is it time?" against `lastSetupBackupAt`.
- **Dedup**: each bundle's content hash (SHA-256 of all entries except `manifest.json`, sorted alphabetically) is compared against the previous successful one. If nothing has changed since last week, the email is **skipped** and only `lastSetupBackupAt` advances. Your inbox doesn't fill up with identical bundles. The hash is exposed as `manifest.contentHash` inside the bundle and persisted across runs as `lastSetupBackupContentHash` in `AppSettings` so the next-week scheduler can skip an identical backup.
- **Failure surfacing**: each failed tick increments `setupBackupConsecutiveFailures` and stores the error message. On the **third consecutive failure** the scheduler emits the `setup-backup:failure-notification` push event exactly once — the Settings panel shows a red banner ("Last 3 backups failed: <reason>") and a toast pops in the foreground. A persistent **"Setup backup is failing — N consecutive failures"** card is also raised in the inbox (and re-raised on every further failure, so it survives a restart and its count stays current) — it clears itself once a send succeeds. See `backup-failure-alert-contract.md`.
- **Re-entrancy**: an in-flight flag prevents two ticks from overlapping. A `force: true` invocation (from "Back up now") bypasses the too-soon and dedup gates but still respects the in-flight lock.

### Restore flow

The Restore dialog runs in two phases — preview, then execute — so you can sanity-check the bundle before any destructive write.

**Preview** (`setup-restore:preview` IPC channel):

1. You pick a `.amc-backup` file from disk. Omniscio reads its first four bytes to detect format.
2. If encrypted, Omniscio runs `decryptBundle(bytes, passphrase)` to recover the plaintext ZIP. Wrong passphrase or tampered byte → AEAD throws, dialog surfaces `Failed to decrypt bundle — wrong passphrase or tampered file`.
3. The ZIP's `manifest.json` is parsed and validated (`version: 1`, schema not newer than current, hostname captured for the cross-machine warning).
4. The dialog shows: bundle hostname, exported-at timestamp, schema version, table row counts, recipe count, skill count, and a list of warnings. The literal warning strings the dialog surfaces are `bundle was created on '<host>' (this machine is '<host>') — review carefully` (cross-machine notice) and `schema mismatch: bundle v<n>, current v<n>; restore may be incomplete or fail` (older-or-newer schema).

**Execute** (`setup-restore:execute` IPC channel) — your choice between two modes:

- **Replace mode** (recommended for "I want my old setup back"):
  1. **VACUUM INTO snapshot first.** Omniscio writes a hot pre-restore snapshot of your current `mission-control.db` to `<userData>/backups/pre-restore-<timestamp>.db`. The last 3 snapshots are kept; older ones are pruned. **If `VACUUM INTO` fails, the restore aborts before any DELETE runs** — you can't lose your current data to a half-finished replace.
  2. Wipes pre-existing rows in the 18 setup tables (DELETE in reverse order with `foreign_keys = OFF` to avoid cascades), then INSERTs every bundle row.
  3. Overwrites your app settings with the bundle's, minus the same denylist that protected the bundle on the way out (so the restore can't accidentally re-introduce a stale OAuth token).
  4. Wipes `~/.claude/recipes/` and writes the bundle's recipes back in.
  5. For each skill in the bundle: wipes the matching folder under `~/.claude/skills/` and writes the bundle's content.

- **Merge mode** (recommended for "I want to add my home machine's setup to my work machine without losing my work setup"):
  1. **No pre-restore snapshot** — merge is non-destructive by design.
  2. `INSERT OR IGNORE` each bundle row. Conflicts on PK keep the **local** copy; the bundle's value is skipped and counted in `result.appliedTables[t].skipped`.
  3. **Settings are preserved unchanged** — `updateSettings` is not called.
  4. **Recipes**: for each `.recipe.json` in the bundle, if a file with the same name already exists locally it's skipped; otherwise written.
  5. **Skills**: same pattern as recipes — existing skill folders are kept, missing ones are pulled from the bundle.

The result object lists every applied table with `bundleRowCount`, `inserted`, `skipped`, and any per-table warnings; the same shape covers settings, recipes, and skills. The dialog renders a green success card with collapsible per-section row counts.

### Restore safety

A passphrase-encrypted bundle still arrives over an untrusted channel (your Gmail account, which a phishing attacker might have compromised). The restore code defends against malicious bundles, not just honest ones.

- **Zip-Slip prevention**: every zip entry path is checked for `..` segments **and** absolute-path prefixes (`/`, `\`, `C:\`-style drive letters) before grouping. After resolving a target write path, a second `path.resolve` defense-in-depth check confirms the result is still inside the destination directory. Any entry that fails either check is skipped and counted in `appliedSkills.skipped_traversal`.
- **Per-skill size cap**: 5 MB per skill, 15 MB total across all skills, 20 MB total bundle. A bundle that exceeds these caps is rejected outright with a clear error.
- **Schema version gate**: a bundle whose `manifest.schemaVersion` is newer than the current DB throws — restoring forward across breaking migrations is unsafe and Omniscio refuses rather than risk corruption. A bundle with an _older_ schema is allowed (schemas are additive) and shows a warning.
- **Manifest validation**: a missing or malformed `manifest.json`, a `version != 1` field, or an unsupported field type throws before any DB write.
- **Empty file**: a zero-byte bundle is rejected immediately.
- **Column whitelist**: every INSERT pulls its column list from `PRAGMA table_info(<table>)` and filters bundle JSON keys against it. A malicious key like `evil; DROP TABLE projects; --` is silently dropped, not interpolated into SQL.
- **VACUUM INTO mandatory in replace mode**: if the snapshot fails (disk full, I/O error, locked DB), the restore throws **before** any DELETE — your current state is preserved. There is no fallback to "skip the snapshot and restore anyway."

### Failure surfacing

The Settings panel reads its state from the `setup-backup:get-status` IPC channel:

- **Last backup**: ISO timestamp from `lastSetupBackupAt`.
- **Next backup**: `lastSetupBackupAt + 7 days`, or "—" if no successful backup has run yet.
- **Encryption**: "Set" if `setupBackupPassphrase` is non-empty, "Not set" otherwise. The plaintext passphrase **never** crosses the IPC boundary out — only a boolean projection.
- **Failure banner**: shown when `setupBackupConsecutiveFailures >= 3`, with the latest error message.

When the threshold is crossed (transition from `<3` to `>=3`), the scheduler emits `setup-backup:failure-notification` once. The Settings panel listens via `useIpcListener` and shows a toast + refreshes its status. Subsequent failures while still over the threshold do **not** re-emit — you don't get a toast every hour for the same broken backup.

### Limits and non-goals

- **Backup, not chat history.** If you need conversation backups, use the automatic local backups (see [Local restore points](#local-restore-points-automatic-backups)) or [Backup Mirror](backup-mirror.md). The setup bundle skips `sessions` and `conversation_messages` on purpose.
- **One bundle per week, dedup-aware.** Not designed as a versioned snapshot system. Gmail keeps every email, so you do effectively get a 52-bundle history per year — but Omniscio won't help you walk through it. Pick the most recent and restore.
- **Single passphrase.** No per-machine passphrases, no key escrow, no recovery code. Lose the passphrase and the bundle is unrecoverable. This is intentional — adding any of those makes the threat model worse, not better.
- **Bundle format v1 only.** A future v2 (richer manifest, signed manifests, etc.) would land as a new `manifest.version` and the restore would gate on it. Today, only v1 is supported.
- **Caps are hard.** A bundle that won't fit is a build error you'll see in the failure banner, not a silent truncation. Trim your skills folder if you blow the 15 MB skills cap.

## For agents

### IPC channels

| Channel                             | Direction              | Purpose                                                                                                                                                                                                              |
| ----------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setup-backup:run-now`              | renderer → main        | Force an immediate backup tick (bypasses the too-soon gate but respects in-flight).                                                                                                                                  |
| `setup-backup:get-status`           | renderer → main        | Fetch the panel state — `enabled`, `hasPassphrase` (boolean projection), `lastBackupAt`, `nextBackupAt`, `consecutiveFailures`, `lastFailureMessage`, last `contentHash`.                                            |
| `setup-restore:preview`             | renderer → main        | Open a file picker, decrypt if needed, parse the manifest, return preview metadata + warnings. The passphrase travels **inward** here so encrypted bundles can be decrypted without first persisting the passphrase. |
| `setup-restore:execute`             | renderer → main        | Run a previously-previewed restore in `replace` or `merge` mode. Returns the per-table / per-recipe / per-skill row counts.                                                                                          |
| `setup-backup:failure-notification` | main → renderer (push) | Threshold-crossing notification — emitted exactly once on the `<3 → >=3` transition.                                                                                                                                 |

The complete handlers live in `src/main/ipc/setup-backup-handlers.ts`.

### CLI access

**This feature's own two actions are desktop-only.** `setup-backup:run-now` and `setup-backup:get-status` are renderer IPC channels with no CLI twin, so this Gmail bundle's "back up now" and its status can only be triggered from the Electron renderer (Settings → Backup & Restore → Setup Backup to Gmail). That is *not* true of backups in general, and the difference matters — **the local restore-point surface IS reachable over the CLI control server** (`http://127.0.0.1:19519`), so an agent or script can list, create, delete and restore backups headlessly:

| Route                       | What it does                                                                                              | Gating                                                                                       |
| --------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `GET /backup`               | Lists the local restore points (manifest metadata only — no credentials).                                  | ordinary CLI auth                                                                            |
| `POST /backup/create`       | Takes a manual local snapshot now. Additive — never touches the live database.                             | ordinary CLI auth, 10/min mutation bucket                                                    |
| `DELETE /backup/:id`        | Deletes one snapshot file. Idempotent.                                                                     | ordinary CLI auth, 10/min mutation bucket                                                    |
| `POST /backup/:id/restore`  | **Overwrites the live `mission-control.db` and relaunches the app.**                                       | **always approval-gated** — the route only enqueues; the restore runs after you approve it  |
| `POST /backup-mirror/restore` | **Restores or merges a Backup Mirror archive** — same destructive reach, same relaunch.                  | **always approval-gated**                                                                     |

Both restores are `NON_TOGGLEABLE`: no setting can un-gate a live-database overwrite, and neither runs until you approve it in your inbox.

### Local restore points (automatic backups)

The routes above act on Omniscio's **automatic local backups** — the "Automatic Backups" section in **Settings → Backup & Restore**, taken when you close Omniscio, periodically while it runs, and on demand (**Snapshot Now**, or `POST /backup/create`). They are a different thing from the Gmail bundle on this page: a restore point holds your whole database, your settings file, the archive of older conversations and a copy of your KMS vault files, and it stays on this computer.

- **Compressed, then encrypted.** A restore point's database, archive and settings files are compressed (zstd) and then encrypted with a key held only on this computer, so on real Omniscio databases they take roughly a quarter of the disk space they did — about 4 times smaller in our measurements. Backups and restores also move data in 1 MB pieces, which measured about 20 times faster than the small default pieces on a busy computer (a 384 MB test backup took 11 seconds instead of 222). The database size shown for each restore point — in Settings, and as `dbSizeBytes` from `GET /backup` — is the compressed size. If the keychain is unavailable, that backup is written as a plain, uncompressed file instead (see [Reclaim disk space](database-compaction.md)).
- **Unchanged vault files are shared, never left as a single copy.** A vault file that has not changed since the previous restore point is linked to that restore point's copy instead of being copied again, but only while that copy is not already shared with another restore point. So once a few restore points exist, every file is still held as at least two separate physical copies, and deleting an old restore point never affects a newer one.
- **Older restore points still restore.** Restore points made by earlier versions — uncompressed, or in the earlier gzip format — restore normally, and a restore point in a format this version does not recognise is refused by name rather than read wrongly.

## Related

- [gmail-integration.md](gmail-integration.md) — the broader Gmail surface in Omniscio; setup backup uses the same OAuth + CLI plumbing (gws or gog).
- [google-integrations.md](google-integrations.md) — Calendar / Drive / Sheets / Gmail single-grant OAuth.
