chezmoiで3台のMacのdotfilesを同期する(秘密はageで暗号化)
オフィス・自宅・ノートの3台のMacでシェル設定と ~/.claude/CLAUDE.md を chezmoi で同期し、 APIキー・SSH秘密鍵は age で暗号化。セットアップコマンド、encryption not configured・ PATHの鶏と卵などの落とし穴、source-of-truth モデルまで。
オフィス・自宅・外出用のノート、3台のMacを行き来して働いている。ある1台でシェル設定や ~/.claude/CLAUDE.md を変更したら、残りの2台も追従するように、しかも秘密は漏らさずに — chezmoi + age で作った記録。
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' # 올리기 한방新しいMacには key.txt(復号の鍵)と chezmoi.toml(ageを使うという設定)の両方 を手動で入れないと復号できない。
なぜ chezmoi か
- シンボリックリンク + bare git: 軽量だが、マシンごとの差分・秘密の管理を自分でやる必要がある。
- Mackup: アプリ設定寄りで、任意の dotfile には柔軟性が低い。
- chezmoi: git バックエンド + マシンごとのテンプレート + age/gpg 暗号化を内蔵 + 衝突時に安全に停止。
秘密の暗号化が内蔵されているのが決め手だった。別ツール不要で chezmoi add --encrypt の一行で済む。
秘密の扱い: ローカルの平文はOK、gitへの刻み込みが危険
~/.zshenv に APIキーが平文で、パーミッションは 644(他のuidも読める)だった。まとめると:
- ローカルの平文自体は実務的にOK。 FileVaultがオンなら、保存時(at rest)の露出は防げる。
- 本当の問題はパーミッション
644。 同じマシンの別uidが読める →chmod 600 ~/.zshenvで終わり。 - 環境変数は「自分の権限で動くすべてのプロセス」が読めるという本質的な限界があるが、これを完全に遮断(keychain注入)するのは個人マシンの脅威モデルには過剰だ。
- 本当の危険は git push で履歴に永久に刻み込まれること → age 暗号化で解決。
age は小さくモダンなファイル暗号化ツール。public key で暗号化すると private key を持つマシンでのみ復号される。--encrypt で追加するとソースは encrypted_private_dot_zshenv.age のように保存され、平文の鍵は見えない。push前にソースディレクトリを一度 grep して秘密が漏れていないか確認する習慣が良い。
何を除外するか — 追加リストと同じくらい重要
~/.claude/ には CLAUDE.md・settings.json(同期対象)以外に 巨大な状態・キャッシュ が混ざっている: projects/ plugins/ cache/ telemetry/ file-history/ shell-snapshots/ sessions/ tasks/ history.jsonl ...。これらはマシンごとに違って当然なので、同期すると衝突・破損する。chezmoi は 明示的に追加したものだけ を管理するので、CLAUDE.md・settings.json・commands/ だけをピンポイントで指定すれば残りは自動的に除外される。マシン固有の値(PATHなど)は ~/.zshrc.local に分離して source する。
[ -f ~/.zshrc.local ] && source ~/.zshrc.local落とし穴
~/.config/chezmoi/フォルダが存在しない。brew installは実行ファイルだけを置く。自分で作って鍵を入れる:mkdir -p ~/.config/chezmoi && chmod 700 ~/.config/chezmoi、その後mv ~/Downloads/key.txt ...(AirDropは Downloads に落ちる。cpではなくmvでコピーを残さないこと)。.age: encryption not configured. clone はできたのに復号に失敗。原因は新しいMacにchezmoi.tomlがないこと。key.txtは鍵、chezmoi.tomlは「ageを使う」という設定 — 両方必要。toml は秘密ではない(公開鍵+パス)ので、作ってchezmoi apply。command not found(PATHの鶏と卵)。 新しいMacは Homebrew の PATH をまだ受け取っていない(受け取るには chezmoi が必要なのに chezmoi が見つからない)。今のシェルだけ:eval "$(/opt/homebrew/bin/brew shellenv)"。.zprofileが入った後は新しいウィンドウで不要。- コピペの改行で切れたコマンド。 長いコマンドの途中に改行が混ざると、zsh は2行目(ファイルパス)を実行するプログラムと勘違いする →
permission denied: /Users/me/.ssh/config。ファイルが壊れたわけではない。一行で貼り付けよう。 - シェルを開くたびに APIキーが画面に流れる。
.zshrcにexport\nPATH=...のように改行が挟まり、引数なしのexportが単独で実行された → zsh では「すべての環境変数を出力」の意味。一行にまとめれば終わり。dotfile の微妙な構文バグは同期に乗って3台に同じように広がる(逆に、一箇所で直せば3台が一緒に直る)。 - 直して push したのに別のMacには古いバージョン。
~/.zshenv(生成物)を直接直してczしたが、ソースが変わっていない → push するものがなく、何も起きなかった。下の source-of-truth 参照。
source-of-truth: 「ファイルを直す = ソースを直す」
chezmoi では ソース(encrypted_..._zshenv.age)が本物で、~/.zshenv はそこから生成された生成物 だ。生成物を直接直しても暗号化されたソースは自動更新されない。だから:
- 意図的な編集は
cze(chezmoi edit --apply)でソースを直接 直す — 生成物に即反映、driftなし。 - 習慣的に生成物を直接直した場合に備えて、
czが push 前にchezmoi re-addで変更を自動キャプチャするようにした。取り込みが必要ならchezmoi add --encrypt ~/.zshenv。
czのre-addは すでに管理下にある ファイルだけをキャプチャする。新しいファイル はchezmoi add(暗号化なら--encrypt)で先に登録しないとソースに入らない。
自動同期: launchd(pull専用)
毎回手で pull するのが面倒で、~/Library/LaunchAgents/com.user.chezmoi-update.plist に RunAtLoad(ログイン時)+ StartInterval で chezmoi update を仕込んだ。周期は 24時間 + ログイン時 — chezmoi update は git fetch 程度で負荷はゼロ、この自動化は「長く触らなくても離れすぎないように」する pull 専用のセーフティネットにすぎない(急ぐならそのMacで直接 pull する)。インターバルの時刻にスリープや電源オフなら、起きたときに1回だけ追いつく。
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.chezmoi-update.plist
launchctl list | grep chezmoi # com.user.chezmoi-update 보이면 OKこの自動化は pull だけ する(自動 push は事故のリスク)。管理下のファイルがローカルで直接編集されていると、chezmoi は上書きせず停止する — 非対話の launchd ではエラーログを残して先に進む安全なデフォルト。
学んだこと
- ローカルの平文自体は正常、gitへの刻み込みだけが危険。 それだけを age で防げばいい。
- age 同期は新しいMacに
key.txtとchezmoi.tomlの2つ が揃っている必要がある。片方だけだとencryption not configured。 - 除外リストが追加リストと同じくらい重要。 キャッシュ・状態を同期すると衝突。chezmoi は指定したものだけを管理するのでピンポイントで。
- chezmoi では 「ファイルを直す = ソースを直す」。
chezmoi editでソースを直す習慣をつければ同期漏れが消える。 - 長いコマンドは一行でコピペ。 切れたコマンドも、環境変数を丸ごと露出したバグも、結局は改行から生まれた。