My work machine is a MacBook, my home desktop runs Linux, and the company handed me a Windows NUC. All three need synced .gitconfig, .zshrc, .tmux.conf β but each OS has quirks. Windows needs sslCAInfo pointing at scoop’s git cert bundle; macOS uses Homebrew; Linux uses apt.
I used to hack it with symlinks and shell scripts. Now I use chezmoi. One dotfiles repo, three machines, one-line setup.
Why Not stow, yadm, or dotbot
Plenty of dotfiles managers exist. chezmoi wins on three fronts:
- Go templates: the same file renders differently per OS, no need to maintain three
.gitconfigvariants - Native encryption: age and gpg are first-class, so secrets can live in a public repo
- onchange scripts: the Homebrew bootstrap only re-runs when the package list actually changes
stow is pure symlinks, no templating. yadm wraps git, templating via plugins. dotbot needs a YAML manifest. chezmoi bundles it all into one binary.
Install and Init
| |
Bootstrap a new machine from an existing repo:
| |
That single line clones the repo, runs the template engine, and writes everything to $HOME.
Filename Attribute System
chezmoi uses filename prefixes to encode behavior. The repo layout itself is the manifest β no separate config needed.
| Prefix | Effect | Example |
|---|---|---|
dot_ | Target is a hidden file | dot_zshrc β ~/.zshrc |
private_ | User-only permissions (0600) | private_dot_ssh β ~/.ssh |
executable_ | Sets executable bit | executable_bin_foo |
encrypted_ | age/gpg encrypted | encrypted_dot_env |
symlink_ | Creates a symlink | symlink_dot_bashrc |
readonly_ | Strips write permissions | readonly_dot_config.toml |
.tmpl suffix | Run through template engine | dot_gitconfig.tmpl |
Prefixes stack. My repo has combinations like:
| |
Templates for Machine Differences
This is chezmoi’s killer feature. My dot_gitconfig.tmpl:
| |
.name and .email come from ~/.config/chezmoi/chezmoi.toml, so each machine can have its own values. The {{ if eq .chezmoi.os "windows" }} block only expands on Windows. On apply, chezmoi strips the .tmpl and writes a clean .gitconfig.
Built-in variables I reach for constantly:
| |
Preview a template without applying:
| |
Age Encryption for Secrets
My repo is public, but it contains an SSH key and database password backups. Those are encrypted with age before they ever hit a commit.
Generate an age key:
| |
Configure ~/.config/chezmoi/chezmoi.toml:
| |
Add files with --encrypt:
| |
The repo only ever stores private_dot_ssh/encrypted_private_id_ed25519.age β opaque ciphertext. On apply, chezmoi decrypts using ~/key.txt and writes the plaintext to the target.
The one catastrophic footgun: key.txt itself must never land in the repo. My workflow: GPG-encrypt it and stash it in a password manager. New machines must restore key.txt manually before running chezmoi init --apply.
run_onchange: Reinstall Only When Lists Change
My .chezmoiscripts/darwin/run_onchange_00_install-packages.sh.tmpl:
| |
The run_onchange_ prefix is the key: chezmoi only runs this script when its content hash changes. Unchanged package list means no re-run β no more five-minute brew install cycles through already-installed tools on every chezmoi apply.
Script naming variants:
| Prefix | When It Runs |
|---|---|
run_once_ | Once per machine, ever, for given content |
run_onchange_ | Whenever the contents change |
run_onchange_before_ | Before file application (install package manager first) |
run_onchange_after_ | After file application (enable fish plugins last) |
The numeric prefix (00_, 01_, 02_) controls execution order.
.chezmoiroot: Source Lives in a Subdirectory
All my files live under home/:
| |
.chezmoiroot tells chezmoi “source files are under home/”. Now the repo root can host a README, install scripts, and other project artifacts without chezmoi trying to apply them as dotfiles.
Great for treating your dotfiles repo like a normal project.
.chezmoiignore: Skip Certain Files
Same syntax as .gitignore, but it supports templates:
| |
Non-macOS machines skip the aerospace window manager config and the Library folder.
Command Cheat Sheet
| |
chezmoi doctor reports the status of encryption tools, template engine, git, and friends. First thing to run when a new machine misbehaves.
Combined with zoxide, fish, and More
My fish config, zoxide init, tmux plugins β all managed by chezmoi. New machine ritual:
- Restore the age key
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply recca0120- run_onchange scripts install CLI tools via brew / apt / scoop
- All configs land in place
- Open fish β zoxide, starship, fzf are already wired up
About 20 minutes end to end, most of it waiting on brew install downloads.
Downsides and Gotchas
chezmoi isn’t free of friction:
- Template learning curve: Go template syntax isn’t beginner-friendly. Whitespace handling with
{{- }}vs.{{ }}takes practice - Painful debugging: template expansion errors are terse β I lean on
chezmoi execute-templateto isolate problems - Age key stewardship: lose the key, lose every encrypted file forever. Back it up separately (I GPG-encrypt and park it in a password manager)
- First apply is destructive: if
$HOMEalready has hand-edited dotfiles, apply overwrites them. Alwayschezmoi difffirst
