Skip to content
Commands

Commands

dothaven is a single static Go binary built on Cobra. It ships the commands below — plus the interactive tui launcher and Cobra’s auto-generated help and completion. This page is the complete reference: purpose, synopsis, every flag with its default, argument rules, and an example for each.

dothaven covers the discovery, audit, and export half of the workflow; chezmoi handles storage, age-encryption, and apply on the target machine. Several commands stop at planning by design — they print what they would do and leave execution to chezmoi.

How output paths are resolved

Backups (backup, and the commands that read them — restore / status / diff) use a stable, cwd-independent location so they always agree on where the latest backup is:

  1. An explicit -o/--output value always wins.
  2. Otherwise, ~/.local/share/dothaven (or $XDG_DATA_HOME/dothaven).

Inspection output (collect snapshots, services / defaults exports) is cwd-aware, since you typically read it where you ran it:

  1. An explicit -o/--output value always wins.
  2. Otherwise, if the current directory is a git repository (a .git/HEAD exists), output goes to <cwd>/reports.
  3. Otherwise, ~/.local/share/dothaven.

Snapshot files are named <hostname>-<timestamp>.json; backups are named backup-<hostname>-<timestamp>. The timestamp is UTC YYYYMMDDHHMMSS.

compare and list read from the literal reports/ directory under the current working directory — they do not use the resolution logic above. Run them from your repo root.

Start here

guide

Answer a few questions, get the exact commands to run.

dothaven guide

Asks what you are trying to do (back up, set up a new computer, reinstall, health check, compare two machines, make a private repo, see what you have) and what kind of work you do (backend, frontend, mobile, devops, data). It answers with an ordered list of commands, a one-line reason for each, a Why for the plan, and notes about what your kind of work should know does not travel. It then offers to run the first step.

It deliberately does not ask what it can detect — whether chezmoi is installed, whether age is configured, whether a backup exists. Those are read from disk. A question whose answer is already known wastes attention and can be answered wrong.

Arguments: none. Requires an interactive terminal.

This command has no flags.

$ dothaven guide
? What do you want to do?  › Reinstall or replace this computer
? Once that's clean, what happens to the setup?  › It moves to another computer

Here's what I'd do:

  1. dothaven ready
     Every repository, checked for changes, commits and stashes that exist on no remote.

Capture

These commands read your machine and write artifacts: a snapshot, a backup, or a security report.

collect

Inventory this machine into a timestamped JSON snapshot.

dothaven collect [flags]

Runs the full collector pipeline (host metadata, the declarative registry, SSH, Ollama, apps, Homebrew, packages, runtimes, editor extensions, fonts, and a dotfiles sweep), redacts secrets by default, and writes a single JSON snapshot. When redaction runs, a summary of what was redacted is printed after the save.

Arguments: none.

FlagDefaultDescription
--no-redactfalseKeep raw values (skip secret redaction).
--slimfalseTruncate long file contents to 10 lines.
-o, --output(resolved)Output directory. Default: ./reports in a repo, else ~/.local/share/dothaven.
$ dothaven collect
Report saved to: /Users/you/project/reports/macbook-20260604120000.json
--no-redact writes raw secret values into the snapshot. Only use it for a snapshot you keep local and never commit.

scan

Scan a file or directory for sensitive data (console).

dothaven scan [path]

Walks a path looking for secrets and prints findings line-by-line with severity, then a summary. Findings are sorted high severity first. If [path] is omitted, the current directory (.) is scanned. A missing path is an error.

Arguments: optional single path (file or directory). Defaults to ..

FlagDefaultMeaning
--no-failfalseAlways exit 0, even with HIGH findings.

Exit code: 2 when anything HIGH is found, so it can gate a commit hook or a CI job (dothaven scan . && git commit). A scanner that always exits 0 can only be read by a human, and the point of scanning is to catch what a human missed.

$ dothaven scan ~/.aws/credentials
~/.aws/credentials
  L3 [High] AWS access key: AKIA****************

security

Write a Markdown security report (default SECURITY.md).

dothaven security [path]

Scans the same way as scan, but writes the result as a Markdown report to disk instead of printing findings, then prints how many files were scanned and how many had findings. If [path] is omitted, the current directory (.) is scanned.

Arguments: optional single path (file or directory). Defaults to ..

FlagDefaultDescription
-o, --outputSECURITY.mdReport output path.
$ dothaven security ./reports -o audit.md
Security report written to: audit.md
  12 scanned, 2 with findings.

Inspect

These commands read existing snapshots (or the live machine) and report — they never write.

ready

Is this Mac safe to wipe? Checks for work that exists nowhere else.

dothaven ready [--depth N] [--root PATH]

Walks your code directories for git repositories and reports uncommitted changes, commits that are on no remote, and stashes. Repositories with no remote at all are listed separately: every commit in them exists only on this machine, and the fix is not “push” but “give it somewhere to be pushed to”. Also reports how old the newest backup is.

Nothing is fetched, so it is fast and works offline — which also means it judges against the remote state git last saw.

Arguments: none.

FlagDefaultMeaning
--depth4How far below each root to look for repositories.
--rootcommon code directoriesWhere to look. Repeatable. Defaults to ~/Developer, ~/Projects, ~/src, ~/code, ~/work and similar, falling back to $HOME.

Exit code: 2 when anything is at risk, so it can gate a wipe script (dothaven ready && diskutil eraseDisk ...).

$ dothaven ready
2 repositories with no remote — these exist ONLY on this Mac:
  ✗ ~/Developer/old-prototype                   412 commits, 3 files uncommitted

1 repository with work not pushed anywhere:
  ⚠ ~/Developer/api                             2 files uncommitted, 5 commits unpushed, 1 stash

36 repositories checked.
  ✓ Newest backup is 2 hours old.

❌ Not safe to wipe: 3 repositories hold work that exists nowhere else.

check

Are my config files still valid? Parses each one.

dothaven check [--all]

Checks every tracked config file that exists on this machine, using the parser that owns its format: encoding/json for JSON, zsh -n / bash -n for shell files, git config --file and ssh -G for their own configs.

No YAML or TOML library is bundled. A config is correct when the program that reads it accepts it, so asking that program is both more accurate than a re-implementation and nothing extra to trust. Formats with no parser to hand are reported as unchecked rather than assumed fine. JSON containing comments (jsonc, which editors allow) is unchecked, not broken.

Arguments: none.

FlagDefaultMeaning
--allfalseAlso list files that passed and files nothing could check.

Exit code: 2 when anything is broken, so it can gate a backup or a commit.

$ dothaven check
  ✗ ~/.config/Code/User/settings.json      invalid character '}' looking for beginning of object key string

15 checked, 13 unchecked (--all to list them).
1 file broken. Fix these before they reach another machine.

list

Print a section (fuzzy-matched) from the most recent report.

dothaven list <section>

Loads the newest .json file in reports/ (relative to the current directory) and prints every section whose name fuzzy-matches the query. Matching is case-insensitive and also matches against the dot-separated parts of a section id (so brew matches apps.brew.bundle).

Arguments: exactly one section query.

This command has no flags.

$ dothaven list packages
[packages.bun.global]
  typescript
  prettier

compare

Diff two JSON snapshots (newest two in reports/ if omitted).

dothaven compare [file1] [file2]

Compares two snapshots and prints only the differences. With no arguments, it picks the two newest .json files in reports/ (relative to the current directory); with fewer than two available it prints usage and exits cleanly. Both explicit files must exist.

Arguments: zero, or exactly two file paths. (One argument is accepted by the parser but falls through to the auto-pick path.)

This command has no flags.

$ dothaven compare reports/old.json reports/new.json
+ packages.bun.global: vitest
- packages.npm.global: eslint

doctor

Compare a snapshot against this machine; list what’s missing.

dothaven doctor <snapshot.json>

Re-inventories the live machine and reports installable items present in the snapshot but missing locally — packages, runtimes, Homebrew formulae, macOS apps, fonts, and editor extensions. Parity is keyed on item name, so version drift is ignored: the question is “present?”, not “same version?”.

Arguments: exactly one snapshot file path.

This command has no flags.

$ dothaven doctor reports/macbook-20260604120000.json
Missing on this machine (present in the snapshot):

  packages.bun.global (1)
    - vitest

1 item(s) missing across 1 section(s).
doctor exits non-zero when anything is missing — it is built for CI. A clean machine prints a parity message and exits 0; any drift exits 1 with the report still on stdout.

Backup and restore

These commands copy tracked config files into a timestamped backup and bring them back. They share --only / --skip category filtering.

backup

Copy tracked config files into a timestamped backup.

dothaven backup [flags]

Collects the registry’s backup targets from your home directory, redacts secrets by default, and copies them into a timestamped backup-<host>-<timestamp> directory (or a .tar.gz with --archive). Prints a per-category file count and, when redaction ran, a redaction summary.

Arguments: none.

FlagDefaultDescription
--no-redactfalseKeep raw values (skip secret redaction).
--archivefalseCreate a .tar.gz instead of a directory.
--encryptfalseEncrypt the archive with age, prompting for a passphrase. Implies --archive.
-o, --output(resolved)Output directory. Default: ~/.local/share/dothaven (stable, cwd-independent).
--only(none)Only these categories (comma-separated).
--skip(none)Skip these categories (comma-separated).
$ dothaven backup --only shell,git
Backup saved to: /Users/you/.local/share/dothaven/backup-macbook-20260604120000
  5 files across: git (2), shell (3)

restore

Restore files from a backup into your home directory.

dothaven restore <backup-path> [flags]

Builds a plan from a backup directory, mapping each backed-up file to its home-directory target, then applies it. New files are written; files that differ from what’s on disk are treated as conflicts and skipped unless --force is given. With --force, a pre-restore snapshot of the files about to be overwritten is saved first. Redacted entries are never restored.

Arguments: exactly one backup-path (a backup directory).

FlagDefaultDescription
--dry-runfalseShow what would change without writing.
--forcefalseOverwrite differing files (a pre-restore snapshot is saved first).
--only(none)Only these categories (comma-separated).
--skip(none)Skip these categories (comma-separated).
$ dothaven restore ~/.local/share/dothaven/backup-macbook-20260604120000 --dry-run

Dry run — no files will be changed:

  [NEW]      git/gitconfig → /Users/you/.gitconfig
  [CONFLICT] shell/zshrc → /Users/you/.zshrc

  2 files total: 1 new, 1 conflicts

restore accepts a backup directory, a .tar.gz, or an age-encrypted .tar.gz.age. An archive is unpacked into a temporary directory (mode 0700) that is removed afterwards; an encrypted one prompts for its passphrase first. Entries that would escape the extraction directory, and symlinks, are refused rather than written.


status

Summarize the latest backup against the live machine.

dothaven status

Finds the newest backup-* directory in ~/.local/share/dothaven and reports how it compares to the live machine: files tracked, modified (conflicts), unchanged, new in backup, and redacted. Modified files are listed by name. If no backup exists, it tells you to run backup first.

Arguments: none.

This command has no flags.

$ dothaven status
Last backup: 2h ago (backup-macbook-20260604120000)
  12 files tracked: 1 modified, 11 unchanged

Modified since backup:
  shell/zshrc

diff

Compare a backup against the live machine, grouped by category.

dothaven diff [backup-path] [flags]

Like status, but prints every entry grouped by category with a per-file status (modified, new, unchanged, redacted) and a colored summary when stdout is a terminal. With no argument it uses the latest backup; pass a backup-path to compare a specific one.

Arguments: optional single backup-path. Defaults to the latest backup.

FlagDefaultDescription
--section(none)Only show this category.
$ dothaven diff --section shell

Comparing backup against live system:

  shell/
    shell/zshrc — modified
    shell/zprofile — unchanged

  2 files: 1 modified, 1 unchanged

Migrate

These commands bridge to chezmoi: they plan (and optionally apply) bringing your configs under chezmoi management, and verify the prerequisites for doing so.

chezmoi-export

Plan (or apply) adding configs to chezmoi, encrypting secrets.

dothaven chezmoi-export [flags]

Builds a chezmoi-add plan — plain add for ordinary configs, add --encrypt for secrets, and add --template for host-varying configs (shell rc, gitconfig, editor settings), whose absolute home paths are rewritten to {{ .chezmoi.homeDir }} so they port across machines — plus a run_onchange install script for Homebrew and global packages. Dry-run by default: it prints the plan and stops. With --apply, it executes against chezmoi (which must be installed, with a configured age key). When the plan encrypts anything it warns that those files are recoverable only with your age key and, on a terminal, asks you to confirm the key is backed up. If your editor’s built-in Settings Sync looks active, it warns that cloud sync and chezmoi will both rewrite those files. On apply it also merges .chezmoiignore patterns for GnuPG runtime cruft when relevant and writes run_onchange_install-packages.sh into the chezmoi source path.

Arguments: none.

FlagDefaultDescription
--applyfalseExecute the plan (default: dry-run).
--pinfalsePin global packages to their captured version.
--only(none)Only these categories/groups (comma-separated).
--skip(none)Skip these categories/groups (comma-separated).
$ dothaven chezmoi-export
chezmoi-export plan — 3 path(s), 1 encrypted:

      add            /Users/you/.gitconfig  (git config)
   🔒 add --encrypt  /Users/you/.ssh/id_ed25519  (ssh private key)
  + run_onchange install script (brew, packages)

Dry-run. Re-run with --apply to execute (requires chezmoi + a configured age key).
age is the encryption backend. Losing the age key means encrypted files are unrecoverable — back the key up somewhere safe and separate from the chezmoi source repo. If chezmoi is not installed, --apply exits non-zero and points you at brew install chezmoi.

init

Check the chezmoi + age prerequisites for export.

dothaven init

A read-only bootstrap check. It probes whether chezmoi is installed, whether age encryption is configured in ~/.config/chezmoi/chezmoi.toml, whether the chezmoi source is an initialized git repo, and your GitHub login (via gh), then prints each step as done () or pending () with the exact command to run. When everything is ready it prints the next chezmoi-export steps. It never changes anything.

Arguments: none.

This command has no flags.

$ dothaven init
dothaven init — chezmoi + age bootstrap

  ✓ chezmoi installed
  → configure age encryption
      chezmoi age setup

Run the commands above, then re-run `dothaven init`.

migrate

Set up this machine from your chezmoi source (prereqs → apply → verify).

dothaven migrate

The clean-machine happy path — one command for the moment you’re staring at an empty laptop. It verifies chezmoi is installed and the source repo is initialized (printing guidance and exiting non-zero if not), warns if age encryption isn’t configured, asks for confirmation on a terminal, then runs chezmoi apply — which pulls your managed configs and runs the generated install script. It finishes by pointing at chezmoi diff and dothaven doctor to verify. On a non-terminal (CI) it skips the confirmation.

Arguments: none.

FlagDefaultMeaning
--dry-runfalseShow what chezmoi would change, via chezmoi diff. Writes nothing.
--yesfalseSkip the confirmation. Required off a terminal.
migrate overwrites files in $HOME and runs your install script. On a terminal it asks first; off one — piped, in a script, over SSH without a TTY — it refuses unless --yes is passed. A pipe cannot answer a question, and silence is not consent.

macOS preferences & local services

Two paired commands for config that doesn’t live in a ~/.dotfile: macOS app preferences (binary plists managed by cfprefsd) and Homebrew service config (under the brew prefix). Both capture to a directory and replay onto a new machine; both are macOS-oriented and no-op gracefully where the underlying tool is absent.

defaults

Capture and restore macOS preferences — both whole app domains and individual system settings.

dothaven defaults export [-o dir]
dothaven defaults import <dir> [--all]

Two mechanisms, because they are good at different things.

Whole-domain plists. export runs defaults export for a curated allowlist of app domains (iTerm2, Terminal.app, Rectangle, Hammerspoon, AltTab) into owner-only .plist files under <dir>/macos-defaults/, and import replays them with defaults import — the safe round-trip for cfprefsd-managed prefs, since a raw file copy is silently ignored. This preserves nested structure such as terminal profiles, which a per-key replay cannot.

Per-key system settings. export also reads every preference domain on the machine (several hundred) and classifies each key into <dir>/macos-defaults/prefs.json. This is what carries natural scroll, key repeat, hot corners, Dock size, keyboard shortcuts and Finder options to a new Mac. Reading is free, so the capture is deliberately wide; what gets written back is not.

Each key lands in one of three buckets:

BucketMeaning
applyA portable scalar. Replayed with defaults write <domain> <key> -<type> <value>.
reviewA real setting pointing into the old machine’s filesystem — where screenshots land, which folder a dialog opens. Captured and listed on import so you can set it by hand; never written.
droppedNot a setting: window frames, recent items, migration markers, launch counters, bare identifiers (boot and display UUIDs — there is no version of one worth setting here), and anything that is not a single value, which is where Spaces layouts live.

dothaven backup runs the same capture and writes prefs.json into the backup folder, so dothaven defaults import <backup-dir> restores a machine’s settings from a plain backup with no separate export step.

Values are run through the same secret scanner as every other captured file, so a token sitting in an app’s preferences is redacted or dropped rather than written to disk.

By default import writes only the core system domainsNSGlobalDomain, Finder, Dock, trackpad and mouse drivers, symbolichotkeys, WindowManager, universalaccess, screencapture, and a handful more. Everything else is captured but held back, because on a real machine the wide list is dominated by application internals: several hundred keys belonging to Outlook, Xcode or a menu-bar widget that nobody chose. --all writes those too.

FlagDefaultDescription
-o, --output (export)(repo ./reports, else ~/.local/share/dothaven)Output directory for macos-defaults/.
--dry-run (import)falseList what would be written. Writes nothing.
--all (import)falseAlso write settings outside the core system domains.
--yes (import)falseSkip the confirmation. Required off a terminal.
defaults import replaces an app preference domain wholesale, and writes system keys one at a time. It summarises both first and asks; off a terminal it refuses unless --yes is passed. Log out and back in for everything to take effect.

services

Capture and restore Homebrew-managed local service config.

dothaven services export [-o dir]
dothaven services import <dir>

export captures user-editable service config under $(brew --prefix)/etc (nginx, httpd, my.cnf, redis, dnsmasq), writes it owner-only, records the source brew prefix, and warns — without redacting, since it must round-trip — if a file looks secret-bearing (e.g. a password in my.cnf). import resolves this machine’s $(brew --prefix) and re-points the old prefix to the new one in the content, so Intel/ARM/Linuxbrew paths resolve. The service binaries themselves come back via the Brewfile; databases and other data are out of scope.

FlagDefaultDescription
-o, --output (export)(repo ./reports, else ~/.local/share/dothaven)Output directory for the services/ tree.
--dry-run (import)falseList the files that would be written, marking which already exist. Writes nothing.
--yes (import)falseSkip the confirmation. Required off a terminal.
services import writes into $(brew --prefix)/etc — outside $HOME, in Homebrew’s own tree. It lists every file first and marks the ones it would overwrite, then asks; off a terminal it refuses unless --yes is passed.

Keeping dothaven current

upgrade

Update dothaven to the latest release.

dothaven upgrade [--check] [--yes]

Also spelled dothaven update. Checks GitHub for the newest release, works out how this copy of dothaven was installed, and runs that installer’s upgrade.

Installed withWhat upgrade runs
Homebrewbrew update && brew upgrade --cask dothaven
go installgo install github.com/doguyilmaz/dothaven/cmd/dothaven@latest
Anything elseNothing — it prints the release page to replace the binary from

brew update comes first because the tap is a git clone that only refreshes on update: upgrading against a stale clone reports “already installed” for a version that has been published for hours.

Flags: --check reports what is available and changes nothing; --yes skips the confirmation prompt.

$ dothaven upgrade
⇡ dothaven 0.4.0 → 0.5.0

Homebrew installed this, so Homebrew replaces it:
  brew update && brew upgrade --cask dothaven

Run it now? [y/N]
dothaven never overwrites its own binary. Homebrew records which version it put in the Caskroom, so a binary that replaces itself leaves that record describing a file which no longer exists — brew outdated keeps reporting the old version, and the next real brew upgrade throws the replacement away. Delegating also means the download is verified against the cask’s pinned checksum by the tool that pinned it.

The update notice

Once a day at most, dothaven checks whether a newer release exists and prints a single line on stderr when there is one:

⇡ dothaven 0.5.0 is available (you have 0.4.0) — run `dothaven upgrade`

Nothing is ever added to stdout, because snapshot output is parsed JSON. The check is skipped entirely when:

  • stderr is not a terminal (a piped or redirected run, so dothaven collect > snap.json stays clean)
  • CI is set
  • the binary is a development build, which has no version to compare
  • DOTHAVEN_NO_UPDATE_CHECK is set to anything at all
The check requests one URL — https://github.com/doguyilmaz/dothaven/releases/latest — and reads the version out of the redirect it answers with. No response body is downloaded, the redirect is never followed, and nothing identifying the machine is sent beyond a dothaven/<version> user agent. The answer is cached in $XDG_CACHE_HOME/dothaven/update-check.json (else ~/.cache/dothaven/), separate from the data directory where backups and snapshots live, and a failed check is silent. Deleting the cache is always safe.

See also