Skip to content
Backup & restore

Backup & restore

This page covers the two kinds of backup, what goes into them, and how restore decides what to write. For the step-by-step move, see Moving to a new machine.

Two kinds of backup

dothaven backupdothaven backup --encrypt
FormA folder (or one .tar.gz with --archive)One .tar.gz.age file
Secret valuesMasked: the key name stays, the value becomes [REDACTED]Kept
SSH keys, cloud logins, other credential filesLeft out, and listedIncluded
Installed apps list, macOS settingsIncludedIncluded
Restores on a new machineEverything except redacted and left-out filesEverything
Use it forA local safety copy, status and diffMoving to a new machine

Both come from the same walk over the same files, with the same MANIFEST.txt. The only differences are the redaction gate and the encryption.

backup

dothaven backup                                  # a folder in ~/.local/share/dothaven
dothaven backup --archive                        # one .tar.gz, still redacted
dothaven backup --encrypt -o /Volumes/MyDrive    # one encrypted file, everything in it

A backup holds:

  • every config file dothaven tracks that exists on this machine (see Registry), plus anything you added with dothaven include;
  • the list of installed apps and packages, a script that reinstalls them, and your crontab (inventory);
  • on macOS, your system settings and the apps in your Dock (macos).

On a terminal, with no --only or --skip, it first shows a category picker (everything selected; categories holding credentials are marked), then offers once any config-looking paths that nothing covers yet. What you pick joins your include list; what you leave is remembered and not asked about again.

Flags

FlagMeaning
--encryptOne age-encrypted file with everything, credentials included (asks for a passphrase)
--archiveOne .tar.gz file instead of a folder (still redacted, not encrypted)
--no-redactKeep raw secret values in a plaintext backup (prefer --encrypt)
-o, --outputWhere to write it, e.g. a USB drive (default: ~/.local/share/dothaven)
--onlyOnly these categories (comma-separated)
--skipSkip these categories, e.g. --skip inventory,macos

The name is backup-<host>-<UTC timestamp>, with .tar.gz or .tar.gz.age for the one-file kinds. The output folder is created owner-only (0700), and every file in a backup is owner-only (0600, or 0700 if it was executable).

Categories

--only and --skip take category names. --skip wins over --only. A name that is not a category is an error that lists the real ones, rather than a backup that quietly holds nothing:

error: unknown category "shel"; choose from: ai, apps, build, bun, cloud, db, dev, devops, dothaven, editor, extra, git, inventory, lang, macos, mobile, net, npm, schedule, secrets, shell, ssh, terminal, vm
CategoryWhat is in it
aiClaude, Codex, Cursor, Gemini… skills, agents, MCP, plugins
shellzsh, bash, fish and their frameworks
gitgit config, global hooks and ignores, gh/glab
editorVS Code, Cursor, Zed, Neovim, Vim, Helix…
terminaltmux, Ghostty, kitty, WezTerm, Starship…
sshssh config, known_hosts and keys
cloudAWS, GCP, Azure, kubectl, Docker, Vercel…
devopshelm, k9s, ansible, terraform…
langRuby, Python, Rust, Go, PHP, .NET, JS tool config
npm, bunnpm and bun config
dbdatabase client config and saved passwords
secrets.netrc, Vault token, GnuPG keys
vmversion pins (.tool-versions, mise, .nvmrc)
netcurl and wget defaults
devdirenv
appsKarabiner, Hammerspoon, window managers…
schedulelaunchd agents (macOS)
buildMaven and Gradle settings
mobileXcode user data, Android keystores
dothavendothaven’s own settings (your include list)
extrapaths you added with dothaven include
inventorylist of installed apps & packages, to reinstall
macossystem settings: trackpad, keyboard, Dock, Finder… (macOS)

inventory and macos are not files; they are selected with the same flags. schedule exists only on macOS, and macOS settings are only captured on a Mac.

What a backup looks like inside

backup-mymac-20260927232938/
├── MANIFEST.txt                   what is in it, and everything left out
├── ai/claude/settings.json
├── ai/claude/skills/…
├── git/.gitconfig
├── git/hooks/pre-commit           executable bit kept
├── shell/.zshrc
├── extra/.config/raycast/…        your includes, by path under $HOME
├── inventory/
│   ├── snapshot.json              installed apps, packages, runtimes, fonts
│   ├── Brewfile
│   ├── install-packages.sh
│   └── crontab
└── macos-defaults/prefs.json      system settings and Dock apps

What is left out, and how you are told

A backup is judged by what it is missing, so dothaven lists everything that exists and did not go in, on screen and in MANIFEST.txt:

Left outWhenWhy
Credential files (~/.ssh, ~/.aws/credentials, kubeconfig, GnuPG, …)Plain backups (unless --no-redact)Masking cannot be trusted to catch every secret in them. They go in the encrypted backup.
Any file with a private key in itPlain backups (unless --no-redact)A private key cannot be partly masked into safety.
Files over 64 MiBEvery backupConfig files are kilobytes; this keeps a stray disk image out.
Text files over 8 MiBPlain backups (unless --no-redact)Too large to check for secrets. The encrypted backup carries them.
Files that exist but cannot be readEvery backupListed on stderr so you can fix permissions.
Folders the new machine rebuilds: node_modules and other dependency folders, caches (.cache, __pycache__, .next, editor caches…), virtual environments, and build output (dist, build, target…)Every backupThey are large and hold no config; npm install, pip or your build brings them back. Build output is left out only beside the file that builds it (package.json with its node_modules, pyproject.toml, Cargo.toml…), so a plugin that ships a built dist/ keeps it.

The rule goes by folder name, not by .gitignore: a folder you keep ignored on purpose (AI tool state, for example) is still carried. A folder you include by its own path is always carried, whatever it is called.

A credential folder stays protected even when you include it yourself: dothaven include ~/.aws does not put ~/.aws/credentials into a plaintext backup.

Some things are skipped without comment because they are not config: sockets, pipes and devices (such as gpg-agent sockets), .DS_Store, and per-tool clutter such as caches, logs, plugin downloads and shell history (each registry entry lists its own).

Symbolic links are followed, so dotfiles managed with GNU Stow or a bare repo are backed up by their content. A link loop cannot trap the walk.

# Left out of this plaintext backup (credentials, high-sensitivity).
# Carry them encrypted: dothaven backup --encrypt  (or chezmoi-export --apply)
#   cloud/aws/credentials
#   ssh

Redaction in plaintext backups

In a plain backup every file is scanned before it is written. Secret values are replaced with [REDACTED] and the rest of the file is kept, so you can still read it. A few formats keep their structure: HostName and IdentityFile in ~/.ssh/config, and _authToken in .npmrc.

A redacted file is kept for reference, but restore never writes it over a real file: that would replace your working token with the word [REDACTED]. See Security for what the scanner detects.

--no-redact

Writes raw values into a plaintext backup, credential files and private keys included. If it wrote any private key, it says so in red:

🔴 1 file holding private keys were written UNENCRYPTED:
    ssh/id_ed25519
  Treat this backup as secret, or use dothaven backup --encrypt instead.

There is rarely a reason to use it. --encrypt gives you the same completeness without the risk.

Encrypted backups

--encrypt implies a single file. It asks for a passphrase twice; it must be at least 10 characters, because the file can hold your SSH keys and cloud logins. In scripts, set DOTHAVEN_PASSPHRASE instead.

  • The files stream through tar, gzip and age encryption straight into the output file. Nothing is written in plaintext, not even temporarily.
  • The file is written as <name>.partial, owner-only from the first byte, and renamed only when complete. An interrupted backup never looks like a finished one.
  • It is a standard age file (passphrase mode), so age -d opens it too. dothaven uses the age library, so neither machine needs the age program.
  • There is no recovery. Without the passphrase, nobody can open the file, including you.

More in Encryption.

include

The registry cannot know every tool. include adds your own paths to every backup:

dothaven include ~/.config/raycast ~/bin   # add
dothaven include --remove ~/bin            # stop carrying one
dothaven include --list                    # what you added, and what nothing covers
dothaven include --review                  # pick interactively from what nothing covers
  • Paths must be inside your home folder. ~/…, $HOME/…, absolute paths and paths relative to where you are all work on the command line.
  • A path that does not exist yet is added anyway, with a warning, and picked up once it does.
  • The list is a text file at ~/.config/dothaven/include, one path per line, # for comments. A line starting with ! is a path you reviewed and declined, so backup does not ask about it again. You can edit the file by hand.
  • Included paths go into the backup under extra/ and restore puts them back in the same place under your home folder, even on a new machine with no include list yet. The include list itself is backed up too (category dothaven).
  • They are scanned and redacted like everything else in a plaintext backup, and restored owner-only.

restore

dothaven restore                                   # pick from the backups it finds
dothaven restore ~/.local/share/dothaven/backup-mymac-20260927232931
dothaven restore /Volumes/MyDrive/backup-mymac-20260927232938.tar.gz.age
dothaven restore github                            # from your private GitHub repo

It accepts a backup folder, a .tar.gz, an encrypted .tar.gz.age, or github (see GitHub sync). The kind is recognised by content, so a file renamed on the way (backup (1).age) still opens. An encrypted one asks for its passphrase (three tries on a terminal) and needs no other tools.

With no path on a terminal, it lists the backups it can find in ~/.local/share/dothaven, the current folder, Downloads, Desktop, Documents, and the top of each mounted drive (and one folder down), newest first, and lets you type a path instead. Off a terminal you must pass a path.

How each file is classified

Restore compares each file in the backup with the one on this machine, and with what it remembers from earlier restores:

MarkStatusMeaningWhat restore does
+newNot on this machineWrites it
↑updatedNewer in the backup; yours is exactly what an earlier restore wroteWrites it (the old copy is saved)
≠differsExists here and is differentAsks you, with a diff. Off a terminal: keeps yours, unless --force
✎changed by youRestore wrote it earlier, and you edited it sinceSame as differs
○skipped last timeYou declined this exact copy before (answered Skip, or unpicked it from the file list)Not offered again, unless you pick it by name or use --force
✓appliedIdenticalNothing
⊘redactedThe backup’s copy had secrets maskedNever written, even with --force

Choosing what to restore

On a terminal, restore shows the counts and asks:

What should be restored?
  Already-applied files are not listed again.
> Everything new or updated (12), ask about 2 that differ
  Choose categories
  Choose files
  Show me the list first
  Cancel
  • Everything new or updated writes the new and updated files and asks about each one that differs.
  • Choose categories picks, for example, only ai, shell and git. The categories you leave out stay on offer, so a restore can be done in phases: shell today, ai tomorrow.
  • Choose files lists every file (type / to filter). Picking a file that differs is your approval to replace it; a file you unpick from that list is remembered as declined.
  • Show me the list first prints the plan, then asks again.

For each file that differs you choose: Overwrite with backup, Skip (keep live file), Show diff, Overwrite all remaining, or Skip all remaining. Esc stops the restore there: that file and the ones after it are left alone, not remembered as skipped, and offered again next time.

It remembers

Restore keeps a ledger at ~/.local/share/dothaven/state/applied.json of what it wrote and what you declined. Running it again shows what is done, instead of offering every file as new work:

  9 files: 1 skipped last time, 5 already the same here, 3 redacted
✓ Nothing new to restore. Everything in the backup is already here, or was left out on purpose.

The ledger holds file hashes, never file contents. Only a decision is remembered: a file you were shown and said no to. A category you did not pick, or a file kept off a terminal because nobody was there to ask, is still on offer next time. What you declined is remembered per backup: restoring a different (for example, newer) backup asks about those files again.

A different home folder

A new Mac often comes with a different account name, and a move to Linux turns /Users/you into /home/you. Every backup records the home folder it was made under. When this machine’s differs, restore writes the new one wherever a text file mentions the old one: a PATH entry, an editor setting, a Claude hook command, includeIf "gitdir:/Users/you/work/". Only a whole path component is changed (/Users/dogu, never /Users/doguyilmaz), and binary files are left as they are. The plan says how many files this touches; --keep-paths turns it off.

Files with no place here

A backup can hold config whose tool lives somewhere else on this OS, or nowhere: Karabiner or iTerm2 restored on Linux, or a tool that a newer dothaven knew about. Restore lists those files beside the plan (in --dry-run too) rather than dropping them silently, and says how to copy them out by hand.

Nothing is lost

Before any file is replaced, its current version is copied to ~/.local/share/dothaven/pre-restore-<timestamp>/, owner-only:

  • the versions it replaced are in ~/.local/share/dothaven/pre-restore-20260927233012

Other safeguards:

  • Files that were credentials or medium-sensitivity config are written owner-only (0600); other config gets 0644. Executable files stay executable, so git hooks keep working. A file that is stricter on this machine (say, a .zshrc you made 0600) keeps its permissions.
  • If the current version of a file cannot be read, it cannot be copied aside, so it is not replaced either; restore says which.
  • If the file on this machine is a symbolic link, restore skips it and says so, rather than writing through the link into whatever it points at.
  • A backup entry that tries to leave its folder (../, absolute paths) is refused. Archives are unpacked into a private temporary folder (0700) that is deleted afterwards, even on a forced exit (a second Ctrl-C). A folder left behind by a crash or kill -9 is removed by a later run once it is an hour old (only folders dothaven made, marked as such). Symbolic links and device files inside an archive are not extracted.
  • Commands that only read a backup’s inventory or settings (missing, reinstall, defaults import) unpack only that part. The rest of an encrypted archive, keys included, is decrypted in memory and read past, never written.

Flags

FlagMeaning
--dry-runShow what would change without writing
--forceOverwrite differing files (a pre-restore snapshot is saved first)
--yesDon’t ask before writing
--onlyOnly these categories (comma-separated). A misspelled one is an error
--skipSkip these categories (comma-separated)
--keep-pathsDon’t rewrite the old machine’s home folder path to this one’s

Off a terminal, or with --yes, there are no questions: new and updated files are written, files that differ are kept (and still offered next time), unless you add --force.

Afterwards

When the files are done, restore offers the rest of the backup. On a terminal it asks whether to put back your macOS settings and whether to reinstall your apps now. Otherwise it prints the commands:

Also in this backup:
  dothaven defaults import ~/Downloads/backup-mymac-….tar.gz.age  # macOS settings
  dothaven reinstall ~/Downloads/backup-mymac-….tar.gz.age        # apps & packages you had
  dothaven missing ~/Downloads/backup-mymac-….tar.gz.age          # what is still missing here

reinstall and missing

Every backup records what was installed. Two commands use that list on the new machine:

dothaven reinstall <backup>   # installs only what is missing
dothaven missing <backup>     # lists what is missing, changes nothing

reinstall covers Homebrew formulae, casks, taps and App Store apps (through the Brewfile), VS Code extensions (through the Brewfile, or from the extension list where there is no Homebrew), Node versions (fnm), global npm/pnpm/bun packages, pipx apps, uv tools, Rust toolchains and cargo crates, Composer, Dart and .NET tools, Cursor extensions, and apt, dnf, pacman, snap and flatpak packages. It shows what is already installed, asks what to install (everything missing, some groups, or single packages), and runs attached to your terminal with no time limit. --dry-run prints the script instead. Off a terminal it needs --yes.

missing matches by name, ignoring versions, and exits with code 1 while anything is missing. Both accept a collect snapshot (.json) as well as a backup. See Commands.

status and diff

dothaven status                    # newest backup folder vs this machine
dothaven diff                      # same, file by file
dothaven diff <backup>             # any backup: folder, archive, encrypted, or github
dothaven diff --section shell      # one category

status compares the newest backup folder in ~/.local/share/dothaven with this machine. If you have no backup folder, only one-file backups (for example encrypted ones, anywhere dothaven looks), it names the newest and suggests dothaven diff <file>, since comparing an encrypted one needs the passphrase.

Last backup: 2h ago (backup-mymac-20260927232931)
  9 files tracked: 1 modified, 5 unchanged
  3 redacted

Modified since backup:
  ~ shell/.zshrc

These differ from the backup. Run dothaven backup to capture them.

diff groups files by category and labels each modified, unchanged, new in backup (missing on machine) or redacted.