Syncing dotfiles across 3 Macs with chezmoi (secrets encrypted with age)
Syncing shell config and ~/.claude/CLAUDE.md across 3 Macs — office, home, and laptop — with chezmoi, while encrypting API keys and SSH private keys with age. Setup commands, pitfalls like encryption not configured and the PATH chicken-and-egg, plus the source-of-truth model.
I work across three Macs — office, home, and a laptop for going out. This is a write-up of using chezmoi + age so that when I change my shell config or ~/.claude/CLAUDE.md on one, the other two follow along, without ever exposing secrets.
TL;DR
# 1) 첫 Mac: 설치 + age 키 생성 (key.txt는 repo 금지, 3대에 수동 운반)
brew install chezmoi age
mkdir -p ~/.config/chezmoi
age-keygen -o ~/.config/chezmoi/key.txt # 출력된 public key(age1...) 메모
chmod 600 ~/.config/chezmoi/key.txt
# ~/.config/chezmoi/chezmoi.toml
# encryption = "age"
# [age]
# identity = "~/.config/chezmoi/key.txt"
# recipient = "age1...(public key)"
# 2) 평문 / 비밀 나눠서 추가
chezmoi add ~/.claude/CLAUDE.md ~/.claude/settings.json ~/.claude/commands \
~/.zshrc ~/.zprofile ~/.gitconfig ~/.ssh/config
chezmoi add --encrypt ~/.zshenv ~/.ssh/id_rsa # age 암호화
chezmoi cd && git init && git add -A && git commit -m "init dotfiles"
git remote add origin git@github.com:<user>/dotfiles.git && git push -u origin main# 일상 별칭 3개
alias czu='chezmoi update' # 받아오기(pull+apply)
alias cze='chezmoi edit --apply' # 소스 편집 + 즉시 반영 (권장 습관)
alias cz='chezmoi re-add && chezmoi git -- add -A && chezmoi git -- commit -m sync && chezmoi git -- push' # 올리기 한방On a new Mac, you have to manually place both key.txt (the decryption key) and chezmoi.toml (the config that tells chezmoi to use age) before anything decrypts.
Why chezmoi
- Symlinks + bare git: lightweight, but you have to manage per-machine differences and secrets yourself.
- Mackup: geared toward app settings, less flexible for arbitrary dotfiles.
- chezmoi: git backend + per-machine templates + built-in age/gpg encryption + stops safely on conflicts.
Built-in secret encryption was the deciding factor. No separate tooling — just one line, chezmoi add --encrypt.
Handling secrets: local plaintext is fine, freezing it into git is dangerous
My ~/.zshenv had API keys in plaintext with permission 644 (readable by other uids too). To sum it up:
- Local plaintext itself is fine in practice. With FileVault on, at-rest exposure is prevented.
- The real problem was the
644permission. Another uid on the same machine can read it → fixed withchmod 600 ~/.zshenv. - Env vars have an inherent limitation that "every process running under my privileges" can read them, but fully blocking that (keychain injection) is overkill for a personal-machine threat model.
- The real danger is having it permanently frozen into history by a git push → solved with age encryption.
age is a small, modern file-encryption tool. Encrypt with a public key and it decrypts only on a machine that holds the private key. Add with --encrypt and the source is stored as something like encrypted_private_dot_zshenv.age, so no plaintext key is visible. It's a good habit to grep the source directory once before pushing to confirm no secrets have leaked.
What to exclude — as important as what to add
Beyond CLAUDE.md and settings.json (the sync targets), ~/.claude/ is mixed with huge state and caches: projects/ plugins/ cache/ telemetry/ file-history/ shell-snapshots/ sessions/ tasks/ history.jsonl .... These are supposed to differ per machine, so syncing them causes conflicts or corruption. chezmoi manages only what you explicitly add, so if you pinpoint just CLAUDE.md, settings.json, and commands/, everything else is excluded automatically. Machine-specific values (PATH, etc.) go into a separate ~/.zshrc.local that you source.
[ -f ~/.zshrc.local ] && source ~/.zshrc.localPitfalls
- The
~/.config/chezmoi/folder doesn't exist.brew installonly lays down the executable. Create it yourself and drop the key in:mkdir -p ~/.config/chezmoi && chmod 700 ~/.config/chezmoi, thenmv ~/Downloads/key.txt ...(AirDrop lands things in Downloads; usemv, notcp, so you don't leave a copy behind). .age: encryption not configured. The clone worked but decryption failed. The cause is that the new Mac has nochezmoi.toml.key.txtis the key,chezmoi.tomlis the "use age" config — you need both. The toml isn't secret (public key + paths), so just create it and runchezmoi apply.command not found(the PATH chicken-and-egg). A new Mac hasn't picked up the Homebrew PATH yet (to pick it up you need chezmoi, but chezmoi can't be found). For this shell only:eval "$(/opt/homebrew/bin/brew shellenv)". Once.zprofileis in place, new windows won't need it.- A command broken by a copy-paste line break. If a newline sneaks into the middle of a long command, zsh mistakes the second line (a file path) for the program to run →
permission denied: /Users/me/.ssh/config. The file isn't broken. Paste it as one line. - API keys spilling onto the screen every time you open a shell. A newline crept into
.zshrclikeexport\nPATH=..., so a bareexportwith no arguments ran on its own → in zsh that means "print all environment variables." Joining it into one line fixes it. A subtle syntax bug in a dotfile rides the sync and spreads identically to all three Macs (conversely, fix it in one place and all three are fixed). - You fixed and pushed it, but another Mac still has the old version. You edited
~/.zshenv(the output) directly and rancz, but the source didn't change → there was nothing to push, so nothing happened. See the source-of-truth section below.
source-of-truth: "editing a file = editing the source"
In chezmoi, the source (encrypted_..._zshenv.age) is the real thing, and ~/.zshenv is an output generated from it. Editing the output directly does not automatically update the encrypted source. So:
- Make intentional edits directly to the source via
cze(chezmoi edit --apply) — reflected in the output immediately, no drift. - To guard against habitually editing the output directly, I made
czauto-capture changes withchezmoi re-addbefore pushing. If you need to fold it back in,chezmoi add --encrypt ~/.zshenv.
cz'sre-addcaptures only files already under management. New files must first be registered withchezmoi add(with--encryptif encrypted) to enter the source.
Automatic sync: launchd (pull only)
Tired of pulling by hand every time, I set up chezmoi update in ~/Library/LaunchAgents/com.user.chezmoi-update.plist with RunAtLoad (at login) + StartInterval. The cadence is 24 hours + at login — chezmoi update is about as heavy as a git fetch, so the cost is zero, and this automation is just a pull-only safety net to keep things from drifting too far when left untouched (if it's urgent, I pull directly on that Mac). If the machine is asleep or off at the interval time, it catches up once on wake.
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.chezmoi-update.plist
launchctl list | grep chezmoi # com.user.chezmoi-update 보이면 OKThis automation only pulls (auto-push is an accident risk). If a managed file was edited locally, chezmoi stops instead of overwriting it — under non-interactive launchd, the safe default is to just leave an error log and move on.
Lessons
- Local plaintext itself is normal; only freezing it into git is dangerous. Just block that with age and you're done.
- age sync requires both
key.txtandchezmoi.tomlpresent on the new Mac. With only one, you getencryption not configured. - The exclude list matters as much as the add list. Syncing caches and state causes conflicts. chezmoi manages only what you name, so pinpoint it.
- In chezmoi, "editing a file = editing the source." Build the habit of editing the source with
chezmoi editand sync misses disappear. - Paste long commands as one line. The broken command and the bug that exposed all environment variables both came down to line breaks in the end.