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:
- An explicit
-o/--outputvalue always wins. - 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:
- An explicit
-o/--outputvalue always wins. - Otherwise, if the current directory is a git repository (a
.git/HEADexists), output goes to<cwd>/reports. - 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 guideAsks 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.
| Flag | Default | Description |
|---|---|---|
--no-redact | false | Keep raw values (skip secret redaction). |
--slim | false | Truncate 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 ..
| Flag | Default | Meaning |
|---|---|---|
--no-fail | false | Always 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 ..
| Flag | Default | Description |
|---|---|---|
-o, --output | SECURITY.md | Report 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.
| Flag | Default | Meaning |
|---|---|---|
--depth | 4 | How far below each root to look for repositories. |
--root | common code directories | Where 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.
| Flag | Default | Meaning |
|---|---|---|
--all | false | Also 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
prettiercompare
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: eslintdoctor
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.
| Flag | Default | Description |
|---|---|---|
--no-redact | false | Keep raw values (skip secret redaction). |
--archive | false | Create a .tar.gz instead of a directory. |
--encrypt | false | Encrypt 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).
| Flag | Default | Description |
|---|---|---|
--dry-run | false | Show what would change without writing. |
--force | false | Overwrite 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 conflictsrestore 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 statusFinds 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/zshrcdiff
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.
| Flag | Default | Description |
|---|---|---|
--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 unchangedMigrate
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.
| Flag | Default | Description |
|---|---|---|
--apply | false | Execute the plan (default: dry-run). |
--pin | false | Pin 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).--apply exits non-zero and points you at brew install chezmoi.init
Check the chezmoi + age prerequisites for export.
dothaven initA 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 migrateThe 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.
| Flag | Default | Meaning |
|---|---|---|
--dry-run | false | Show what chezmoi would change, via chezmoi diff. Writes nothing. |
--yes | false | Skip 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:
| Bucket | Meaning |
|---|---|
apply | A portable scalar. Replayed with defaults write <domain> <key> -<type> <value>. |
review | A 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. |
| dropped | Not 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 domains — NSGlobalDomain, 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.
| Flag | Default | Description |
|---|---|---|
-o, --output (export) | (repo ./reports, else ~/.local/share/dothaven) | Output directory for macos-defaults/. |
--dry-run (import) | false | List what would be written. Writes nothing. |
--all (import) | false | Also write settings outside the core system domains. |
--yes (import) | false | Skip 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.
| Flag | Default | Description |
|---|---|---|
-o, --output (export) | (repo ./reports, else ~/.local/share/dothaven) | Output directory for the services/ tree. |
--dry-run (import) | false | List the files that would be written, marking which already exist. Writes nothing. |
--yes (import) | false | Skip 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 with | What upgrade runs |
|---|---|
| Homebrew | brew update && brew upgrade --cask dothaven |
go install | go install github.com/doguyilmaz/dothaven/cmd/dothaven@latest |
| Anything else | Nothing — 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]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.jsonstays clean) CIis set- the binary is a development build, which has no version to compare
DOTHAVEN_NO_UPDATE_CHECKis set to anything at all
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.