The problem
Every time, When I got a new Macbook / Linux Machine (an office work, a VPS), I end up spending hours setting up terminal from scratch. Installing
zsh, zinit, git, configuring SSH keys, setting up plugins, aliases and shell functions…I tried solving this with a bootstrap script. It worked well enough, it installed everything:
brew, git, docker , etc. But installing programs is only half the battle. It didn't restore my configuration files. I still had to set up nvm, and redefine all my aliases and functions from scratch every time.Why not stow
With
GNU Stow, configuration files are simply symlink, allowing immediate config updates. However, this introduces an annoying issue: authentication files for CLI tools like Claude Code and GitHub Copilot are also stored within the symlink directories. Consequently, every time I push to Git, I am forced to review the changes to ensure no secrets are included. On several occasions, I have accidentally pushed these sensitive files to GitHub and had to reset those commits.
The other pain point: when I updated a config on one machine, I had to manually mirror that change on all the others. If I forgot, the machines would quietly drift apart, and I'd end up with different behavior depending on where I was working
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
References
Introducing chezmoi
Chezmoi is an open-source tool managing your
dotfiles across multiple machine, securely.The concept is simple: you tell
chezmoi which config files to tack, and it stores them in a Git repository that you push to Github (or any remote). Frome there, syncing your config to a new machine is just a matter of cloning and applying.On a new machine, I either run the one line:
chezmoi init --apply $GITHUB_USERNAME
# or
chezmoi init --apply https://github.com/$GITHUB_USERNAME/dotfilesOr after my bootstrap script install everything:
chezmoi init
chezmoi applyWhen I update a config on a config on one machine:
chezmoi add ~/.zshrc
chezmoi edit ~/.zshrc
chezmoi re-addPushed to Github:
chezmoi cd
git commit -m 'chore: update zsh config'
git pushAutomatically commit and push changes to your repo:
[git]
autoCommit = true
autoPush = trueOn another machine will pull the latest changes from your repo
chezmoi update
chezmoi git pull -- --autostash --rebase && chezmoi diffIf you happy with changes, then you can run to apply them:
chezmoi applyRestart the terminal and it’s done. This has been especially useful when spinning up AWS EC2 instances. What used to take hours now takes minutes.
Documentation: User Guide
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_ | Set executable bit | executable_bin_foo |
encrypted_ | age/gpg encrypted | encryped_dot_env |
symlink_ | Create a symlink | symlink_dot_bashrc |
readonly_ | Strips write permissions | readonly_dot_config.yaml |
.tmpl suffix | Run through template engine | dot_gitconfig.tmpl |
Template for Machine Differences
This is chezmoi’s killer feature.
[user]
name = {{ .username | quote }}
email = {{ .email | quote }}
[core]
symlink = true
paper = delta
dot_config/git/config.tmpl
.username , .email come from ~/.config/chezmoi/chezmoi.yaml . So each machine can have its own value.If you want to add the prompt input to enter the value for each machines
{{- $email := "" -}}
{{- if hasKey . "email" -}}
{{- $email = .email -}}
{{- else -}}
{{- $email = promptString "Email address" -}}
{{- end -}}
{{- $username := "" -}}
{{- if hasKey . "username" -}}
{{- $username = .username -}}
{{- else -}}
{{- $username = promptString "Username" -}}
{{- end -}}
data:
email: {{ $email | quote }}
username: {{ $username | quote }}.chezmoi.yaml.tmpl
On apply, chezmoi strips the
tmpl and writes a clean .config/git/config Built-in variables reach for constantly:
{{ .chezmoi.os }} # darwin/linux/windows
{{ .chezmoi.osRelease }} # information from /etc/os-release
{{ .chezmoi.arch }} # amd64/arm64
{{ .chezmoi.hostname }} # machine name
{{ .chezmoi.username }} # login user
{{ .chezmoi.homeDir }} # home directorySee more: Variables - chezmoi
Preview a template without applying:
chezmoi execute-template < dot_config/git/config.tmpl
Encrypt for secret configuration
Your dotfiles repo maybe public, but it can contains a SSH key, Database password backup, token,… Those must be encrypted with age (or gpg) before they ever hit a commit
Generate an age private key encrypted with a passphrase in the file
key.txt.age with the command:chezmoi age-keygen | chezmoi age encrypt --passphrase --output=key.txt.age
Public key: age1examplepublickeyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Enter passphrase (leave empty to autogenerate a secure one):
Confirm passphrase:
# add key.txt.age to chezmoiignore so that chezmoi does not try to create it
echo key.txt.age >> .chezmoiignoreConfigure chezmoi to decrypt the passphrase-encrypted private key
cat > run_onchange_before_decrypt-private-key.sh.tmpl <<EOF
#!/bin/sh
if [ ! -f "${HOME}/.config/chezmoi/key.txt" ]; then
mkdir -p "${HOME}/.config/chezmoi"
chezmoi age decrypt --output "${HOME}/.config/chezmoi/key.txt" --passphrase "{{ .chezmoi.sourceDir }}/key.txt.age"
chmod 600 "${HOME}/.config/chezmoi/key.txt"
fi
EOFConfigure
chezmoi.yaml.tmpl :encryption: "age"
age:
identity: "{{ .chezmois.source }}/chezmoi/key.txt"
recipient: "age1examplepublickeyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Add file with
--encrypt argumentschezmoi add --encrypt ~/.ssh/id_ed25519The repo only ever stores
private_dot_ssh/encrypt_private_id_ed25519.age - opaque cipher text. On apply decrypts using ~/key.txt and writes the plain text to the target.If you want to add the
key.txt to dotfiles repo. You have to the encrypt with the passphrase age -p ~/key.txt > ~/key.txt.age
chezmoi add ~/key.txt.ageCommand Cheatseat
chezmoi add ~/.zshrc # track an existing file
chezmoi add --encrypt # track encrypted
chezmoi edit # edit the source file directory
chezmoi diff # show pending changes
chezmoi apply # apply the changes dotfiles
chezmoi apply --dry-run -v # preview without writing
chezmoi cd # go to chezmoi source directory
chezmoi update # pull latest update + apply
chezmoi doctor # check healthMy Github repo
I’m sure there are other ways to manage
dotfiles , but this is what’s been working for me. Let me known how you handle yours.