Skip to content
Backup & restore

Backup & restore

dothaven backup copies your tracked config files into a timestamped tree, applying the same secret-scrubbing gate as collect so a plaintext backup never carries a raw secret. dothaven restore reads such a tree back, classifying each file against the live machine and writing only what is safe to write. status and diff let you inspect a backup without touching anything.

This is a local copy workflow — no encryption, no chezmoi, no remote. For the encrypted-storage path, see the hybrid model below.

backup

dothaven backup

backup walks the registry of tracked targets, reads each source, runs it through the redaction gate, and writes the result into a fresh directory:

backup-<hostname>-<UTC-timestamp>/

The timestamp is YYYYMMDDhhmmss in UTC, so directory names sort chronologically. If the hostname can’t be read it falls back to machine.

Where backups land

Backups go to a stable, cwd-independent location so restore, status, and diff always find them no matter where you run them from:

  • an explicit -o / --output wins;
  • otherwise ~/.local/share/dothaven (or $XDG_DATA_HOME/dothaven).

(Unlike collect snapshots and services/defaults exports, which stay cwd-aware — ./reports inside a git repo — since they’re inspection output you read in place, not a backup another command must locate later.)

dothaven backup -o ~/dotfiles-backups

Example run:

$ dothaven backup
Backup saved to: /Users/you/.local/share/dothaven/backup-mbp-20260604091233
  6 files across: git (2), shell (3), ssh (1)

(The per-category line is rendered from the actual category counts; git (2), shell (3) means two files in the git category, three in shell.)

The MANIFEST

Every backup tree includes a MANIFEST.txt (written before any archiving, so it travels with the backup). It records the source host and OS, the dothaven version, whether redaction was on, the per-category file counts, the high-sensitivity paths that were deliberately excluded, and the restore command. The exclusion list is the safety-critical part: a backup you can’t audit for completeness is dangerous, so it lives inside the backup rather than scrolling past once in the console.

On a terminal, backup first prints a reminder that it captures saved config, not unsaved editor buffers or in-memory state. Save and quit your editors before relying on a backup to wipe a machine — hot-exit drafts and unsaved tabs are not on disk and are not captured.

The copy engine

Each registry target is either a single file or a directory:

  • Files are read, gated, and written to their mapped destination.
  • Directories are mirrored recursively (dotfiles included); every file inside passes through the gate independently.

Missing or unreadable sources are skipped silently — a tool you don’t have installed simply contributes nothing rather than erroring. If nothing matches at all, you get:

No files found to backup.

The redaction / skip gate

Before any file is written, its content goes through a two-tier gate:

  1. Skip (private keys). If the scanner flags a skip-action finding — a PEM or PGP private key block — the file is not copied at all. A plaintext backup never carries a raw private key.
  2. Redact (secrets). Otherwise, matched secrets (tokens, API keys, passwords, AWS/GCP credentials, and the like) are masked in place with a [REDACTED] marker before the file is written. The surrounding config is preserved; only the secret value is replaced.

After a redacting run, a sensitivity report is printed summarizing what was scrubbed or skipped:

⚠ Sensitivity report:
  High   ssh/config                     IP address — redacted
  High   ssh/id_ed25519                 private key — skipped

  1 items redacted, 1 skipped. Use --no-redact to include all.

--no-redact

Disables the gate entirely — raw values are kept, private keys included. Use this only for a backup you intend to keep private and unshared.

dothaven backup --no-redact

With --no-redact, no sensitivity report is printed (nothing was scrubbed).

--archive

Writes a single .tar.gz instead of a directory and removes the intermediate tree:

dothaven backup --archive
$ dothaven backup --archive
Archive saved to: /Users/you/.local/share/dothaven/backup-mbp-20260604091233.tar.gz
  6 files across: git (2), shell (3), ssh (1)
Archives are not picked up by restore, status, or diff — those operate on backup directories. Extract a .tar.gz first if you need to diff or restore from it.

--only / --skip

Both flags take comma-separated category names. --skip always wins; a non-empty --only restricts the run to just those categories.

dothaven backup --only shell,git
dothaven backup --skip ssh

backup flags

FlagDescription
-o, --outputOutput directory (default: ~/.local/share/dothaven)
--archiveCreate a .tar.gz instead of a directory
--no-redactKeep raw values (skip secret redaction)
--onlyOnly these categories (comma-separated)
--skipSkip these categories (comma-separated)

restore

dothaven restore <backup-path>

restore takes a backup directory and rebuilds a plan: it walks the backup, maps each file back to its live target on the machine, reads that target’s current content, and assigns a status.

Per-file status

StatusMeaning
newIn the backup, absent on the machine
conflictPresent on the machine but differs from the backup
sameIdentical — nothing to do
redactedThe backup holds a [REDACTED] marker — unrestorable

Default behavior

By default, restore is conservative:

  • new files are written.
  • conflict files are skipped — your existing file is left untouched.
  • same and redacted files are never written.

Three safety guarantees apply to every write:

  • Owner-only perms for secrets. A file from a medium/high-sensitivity registry entry is written 0600, so a secret restored from a --no-redact backup never lands world-readable; ordinary configs keep 0644.
  • Path containment. A backed-up path that would resolve outside its target directory (a ../ traversal in a crafted backup) is refused — restore never writes outside the destination tree.
  • Symlink refusal. If a live target is a symlink, restore skips it rather than following it (which would modify whatever it points at) and reports the count so you can resolve it by hand.
$ dothaven restore ~/backups/backup-mbp-20260604091233
Restored 2 file(s) across: shell (2)
  4 file(s) skipped

If nothing was restored because everything differs, you’re told how to proceed:

No files restored. 3 conflict(s) skipped — re-run with --force to overwrite.

If there was simply nothing to do:

No files restored (everything already up to date).
A [REDACTED] file is never restored. When a backed-up file contains the [REDACTED] marker, its real value was stripped during backup. Restoring it would overwrite a live secret with the placeholder text, so the restore engine refuses — even under --force. The redacted gate protects you on the way in and on the way out.

--force

Overwrites conflict files. Before overwriting, the prior content of each conflicting file is snapshotted into a pre-restore-<timestamp> directory (resolved the same way as backup output), so an overwrite is always reversible.

dothaven restore <backup-path> --force
$ dothaven restore ~/backups/backup-mbp-20260604091233 --force
Pre-restore snapshot saved to: /Users/you/project/reports/pre-restore-20260604093001
Restored 5 file(s) across: git (2), shell (3)

--force does not override the redacted guard — redacted (and same) entries are still skipped.

--dry-run

Prints the full plan without writing anything:

dothaven restore <backup-path> --dry-run
$ dothaven restore ~/backups/backup-mbp-20260604091233 --dry-run

Dry run — no files will be changed:

  [NEW]      shell/.zshrc → /Users/you/.zshrc
  [CONFLICT] git/.gitconfig → /Users/you/.gitconfig
  [SAME]     shell/.bashrc → /Users/you/.bashrc
  [REDACTED] ssh/config → /Users/you/.ssh/config

  4 files total: 1 new, 1 conflicts, 1 unchanged, 1 redacted (skipped)

--only / --skip

Same category filtering as backup — comma-separated, --skip wins over --only.

dothaven restore <backup-path> --only shell
dothaven restore <backup-path> --skip ssh

If filtering leaves nothing:

No restorable files found in backup.

restore flags

FlagDescription
--dry-runShow what would change without writing
--forceOverwrite differing files (a pre-restore snapshot is saved first)
--onlyOnly these categories (comma-separated)
--skipSkip these categories (comma-separated)

status

dothaven status

status summarizes your latest backup directory against the live machine. It finds the newest backup-* directory in the output location (archives are ignored — they can’t be diffed without extraction) and tallies the plan:

$ dothaven status
Last backup: 2h ago (backup-mbp-20260604071205)
  6 files tracked: 1 modified, 4 unchanged
  1 not on machine (new in backup)

Modified since backup:
  git/.gitconfig

The age is rendered coarsely as minutes / hours / days ago from the directory’s mtime. When nothing has drifted:

  Everything up to date.

If no backup exists yet:

No backup found. Run 'dothaven backup' first.

diff

dothaven diff [backup-path]

diff compares a backup against the live machine, grouped by category. With no argument it uses the latest backup; pass a path to compare a specific one.

$ dothaven diff

Comparing backup against live system:

  git/
    git/.gitconfig — modified
  shell/
    shell/.bashrc — unchanged
    shell/.zshrc — new in backup (missing on machine)
  ssh/
    ssh/config — redacted

  4 files: 1 modified, 2 unchanged, 1 new, 1 redacted

On a TTY the status labels are colored (yellow = modified, blue = new, green = unchanged, gray = redacted); piped output is plain.

--section

Limit the comparison to a single category:

dothaven diff --section git
No entries found for section: git

is printed when that category has no entries in the backup.

diff flags

FlagDescription
--sectionOnly show this category

The hybrid model

backup/restore is the local plaintext path: a redacted copy you can inspect, archive, and roll back yourself. For long-term, encrypted storage, dothaven hands off to chezmoi:

  • dothaven does discovery, audit, and export.
  • chezmoi does storage, age-encryption, and apply.

age is the encryption backend on the chezmoi side. Losing the age key means the encrypted files are unrecoverable — back that key up separately. The two paths are complementary: use backup/restore for quick local snapshots, and the chezmoi export for an encrypted source of truth.