chezmoi로 3대 Mac dotfiles 동기화하기 (비밀은 age로 암호화)
사무실·집·맥북 3대 Mac 사이에 셸 설정과 ~/.claude/CLAUDE.md 같은 전역 설정을 chezmoi로 동기화하고, API 키·SSH 개인키는 age로 암호화한 과정과 삽질 기록
사무실, 집, 외출용 맥북 — Mac 3대를 오가며 일한다. 한 곳에서 셸 설정이나 ~/.claude/CLAUDE.md(Claude Code 전역 지침)를 고치면 나머지 두 대에도 따라오게 만들고 싶었다. 이 글은 그 과정에서 한 고민·결정·삽질을 그대로 정리한 기록이다. 똑같이 여러 Mac을 쓰는 사람에게 도움이 되길.
목표와 제약
- 무엇을: 셸 dotfiles(
~/.zshrc등),~/.gitconfig,~/.ssh/config, 그리고 핵심인~/.claude/CLAUDE.md. - 어떻게: 한 곳에서 고치면 나머지가 따라오게. 단, 실시간 동기화는 필요 없다 — git pull 방식으로 충분.
- 문제: 이 중 일부엔 비밀이 섞여 있다.
~/.zshenv엔 API 키가,~/.ssh/id_rsa엔 개인키가. 이걸 git repo에 평문으로 올리면 영구 노출된다.
왜 chezmoi인가
dotfiles 동기화 방법은 여러 가지다.
- 심볼릭 링크 + bare git repo: 가볍지만 머신별 차이(PATH 등) 처리와 비밀 관리를 직접 해야 한다.
- Mackup: 앱 설정 위주라 임의 dotfile엔 덜 유연.
- chezmoi: git 백엔드 + 머신별 템플릿 + age/gpg 암호화 내장 + 충돌 시 안전하게 멈춤.
비밀 암호화가 내장돼 있다는 점이 결정적이었다. 별도 도구 없이 chezmoi add --encrypt 한 줄이면 된다.
핵심 고민: 비밀을 어떻게 다룰까
작업을 시작하자마자 막은 지점. ~/.zshenv에 API 키 5개가 평문으로 있었고, 파일 권한은 644(다른 사용자도 읽힘)였다.
로컬에 평문으로 둬도 괜찮은가?
먼저 짚어야 할 질문. 결론부터 말하면 권한만 600으로 조이면, 개인 Mac에선 평문이어도 실무적으로 괜찮다.
- FileVault가 켜져 있으면 디스크가 암호화돼 있어 Mac을 도난당하거나 꺼진 상태에선 못 읽는다. "저장 상태(at rest)" 노출은 막힌다.
- 권한
644가 진짜 문제였다. 같은 머신의 다른 uid(게스트 계정·비admin 데몬 등)가 읽을 수 있다.chmod 600으로 나만 읽게 바꾸면 끝. - 다만 환경변수의 본질적 한계는 남는다. env var는 "내 권한으로 실행되는 모든 프로세스"가 읽을 수 있다. 악성 npm 패키지 같은 게 내 권한으로 돌면 파일 권한과 무관하게 읽는다. 이걸 완전히 막으려면 1Password/keychain 주입까지 가야 하는데, 개인 머신 위협모델엔 과하다.
chmod 600 ~/.zshenv정리하면 진짜 위험은 로컬 평문이 아니라 git push로 히스토리에 영구 박제되는 것이고, 그건 암호화로 해결한다.
age 암호화 선택
선택지는 (a) age 암호화, (b) 비밀을 .local 파일로 분리, (c) 키 로테이션 먼저였다. 가장 단순하고 키를 그대로 유지하는 age 암호화를 골랐다.
age는 작고 현대적인 파일 암호화 도구다. 키 쌍을 만들고, public key로 암호화하면 private key를 가진 머신에서만 복호화된다.
무엇을 동기화하고 무엇을 제외할까
~/.claude/ 디렉토리가 함정이다. CLAUDE.md나 settings.json은 동기화해야 하지만, 그 안엔 거대한 상태·캐시도 같이 있다.
제외해야 할 것 (동기화하면 충돌·손상):
~/.claude/ 의: projects/ plugins/ cache/ telemetry/ file-history/
backups/ shell-snapshots/ sessions/ tasks/ history.jsonl ...
이건 Claude Code가 실시간으로 쓰는 상태라 머신마다 달라야 정상이다. chezmoi는 명시적으로 추가한 것만 관리하므로, CLAUDE.md·settings.json·commands/만 콕 집어 넣으면 나머지는 자동으로 제외된다.
머신별로 다른 값(고유 PATH 등)은 ~/.zshrc.local로 분리해 .zshrc에서 source한다.
[ -f ~/.zshrc.local ] && source ~/.zshrc.local셋업: 첫 번째 Mac
# 1) 설치
brew install chezmoi age
# 2) age 키 생성 — "루트 비밀". repo엔 절대 안 넣고 3대에 수동 운반한다.
mkdir -p ~/.config/chezmoi
age-keygen -o ~/.config/chezmoi/key.txt # 출력된 public key(age1...) 메모
chmod 600 ~/.config/chezmoi/key.txt~/.config/chezmoi/chezmoi.toml로 age 설정을 알려준다.
encryption = "age"
[age]
identity = "~/.config/chezmoi/key.txt"
recipient = "age1...(위 public key)"평문 파일과 비밀 파일을 나눠 추가한다.
# 평문
chezmoi add ~/.claude/CLAUDE.md ~/.claude/settings.json ~/.claude/commands \
~/.zshrc ~/.zprofile ~/.gitconfig ~/.ssh/config
# 비밀 — age 암호화로
chezmoi add --encrypt ~/.zshenv ~/.ssh/id_rsa암호화된 소스는 encrypted_private_dot_zshenv.age처럼 저장되고, 안을 열어보면 평문 키는 보이지 않는다. push 전에 소스 디렉토리를 한 번 더 grep해서 비밀이 평문으로 새지 않았는지 확인하는 습관이 좋다.
chezmoi cd
git init && git add -A && git commit -m "init dotfiles"
git remote add origin git@github.com:<user>/dotfiles.git
git branch -M main && git push -u origin main두 번째 Mac: 여기서 삽질이 시작된다
핵심은 age key.txt는 repo에 없다는 것. 이건 손으로 옮겨야 한다(AirDrop·scp·1Password). 이 과정에서 헷갈린 지점들을 그대로 적는다.
함정 1: ~/.config/chezmoi/ 폴더가 없다
brew install chezmoi는 실행파일만 깐다. ~/.config/chezmoi/ 폴더는 설치로 생기지 않는다. 직접 만들고 거기에 키를 넣어야 한다.
mkdir -p ~/.config/chezmoi && chmod 700 ~/.config/chezmoi
mv ~/Downloads/key.txt ~/.config/chezmoi/key.txt # AirDrop은 Downloads로 떨어진다
chmod 600 ~/.config/chezmoi/key.txt
cp가 아니라mv로. Downloads에 비밀 키 사본을 남기지 말 것.
함정 2: encryption not configured
chezmoi init --apply를 돌렸더니:
chezmoi: .ssh/id_rsa.age: encryption not configured
clone은 됐는데 .age 파일을 풀 수가 없다는 뜻. 원인은 chezmoi.toml이 새 Mac에 없어서다. key.txt는 복호화 열쇠, chezmoi.toml은 "age를 쓰라"는 설정 — 둘 다 있어야 복호화된다. toml은 비밀이 아니니(공개키+경로뿐) 그냥 만들면 된다.
cat > ~/.config/chezmoi/chezmoi.toml <<'EOF'
encryption = "age"
[age]
identity = "~/.config/chezmoi/key.txt"
recipient = "age1...(public key)"
EOF
chezmoi apply # clone은 이미 됐으니 apply만 다시chezmoi apply가 아무 메시지 없이 끝나면 성공이다. chezmoi는 문제가 있을 때만 떠든다.
함정 3: command not found — PATH 닭-달걀
새 Mac에선 Homebrew의 /opt/homebrew/bin을 PATH에 넣는 게 보통 내 .zprofile인데, 그게 아직 적용 안 됐다(그걸 받으려면 chezmoi가 필요한데 chezmoi를 못 찾는 상황). 이번 셸에서만 즉시 로드하면 된다.
eval "$(/opt/homebrew/bin/brew shellenv)"chezmoi init --apply로 .zprofile이 깔리고 나면, 새 터미널 창부턴 이 줄 없이도 동작한다.
함정 4: 복붙하다 줄바꿈으로 끊긴 명령
가장 자주 당한 것. 긴 명령을 복사하다 중간에 엔터가 섞이면, zsh가 둘째 줄(예: 파일 경로)을 실행할 프로그램으로 착각한다.
zsh: permission denied: /Users/me/.ssh/config
config 파일이 망가진 게 아니라, 그게 별도 줄로 떨어져 실행되려다 난 에러다. 한 줄로 붙여넣자.
자동 동기화: launchd
매번 손으로 당기기 귀찮으니, 백그라운드에서 주기적으로 받아오게 했다. macOS는 launchd LaunchAgent를 쓴다.
~/Library/LaunchAgents/com.user.chezmoi-update.plist에 RunAtLoad(로그인 시) + StartInterval(주기)로 chezmoi update를 걸었다.
"로그인 시"가 무슨 뜻인가, 안 켜면 어떻게 되나
- "로그인 시" = macOS 계정 로그인(부팅 후 암호 입력) 시점. 화면 잠금 해제나 sleep에서 깨우는 건 아니다.
- 인터벌 시각에 Mac이 자거나 꺼져 있으면 그땐 안 돌고, 깨어나거나 로그인할 때 1회 따라잡는다(놓친 만큼 몰아 돌진 않음).
- 대부분 로그아웃을 잘 안 하고 뚜껑만 닫으니, 로그인 상태가 유지돼 인터벌이 계속 돈다.
주기는 6시간? 24시간?
처음엔 6시간을 생각했지만 24시간 + 로그인 시로 정했다. chezmoi update는 git fetch 수준이라 부담은 0에 가깝지만, 이 자동화는 **"받아오기(pull) 전용 안전망"**일 뿐이다. 지금 당장 다른 Mac에 반영하고 싶으면 어차피 그 Mac에서 직접 당긴다. 백그라운드는 "오래 안 건드려도 너무 벌어지지 않게" 하는 보조 역할이라 하루 1회면 충분하다.
중요: 이 자동화는 pull만 한다. 내가 고친 걸 자동 push하진 않는다(자동 push는 사고 위험). 그리고 로컬에서 직접 수정한 관리 파일이 있으면 chezmoi가 덮어쓰지 않고 멈춘다 — 비대화형 launchd에선 그냥 에러 로그만 남기고 넘어간다. 안전한 기본값이다.
등록은 머신마다 1회.
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.chezmoi-update.plist
launchctl list | grep chezmoi # com.user.chezmoi-update 보이면 OK일상 워크플로우
별칭 두 개로 끝난다.
alias czu='chezmoi update' # 받아오기(pull+apply)
alias cz='chezmoi cd && git add -A && git commit -m sync && git push; cd -' # 올리기 한방- 고칠 땐
chezmoi edit ~/.claude/CLAUDE.md(관리 파일을 직접 고치면 다음 apply 때 되돌려질 수 있으니). - 올릴 땐
cz, 다른 Mac에서 받을 땐czu(또는 자동).
새 파일은 주의.
cz(=git add -A)는 이미 chezmoi 소스에 들어온 파일의 변경만 올린다. 디렉토리째 관리해도 그 안의 새 파일은chezmoi add로 먼저 등록해야 소스로 복사된다.
chezmoi update는 파일을 지우고 다시 받나?
아니다. 디렉토리째 삭제 후 재다운로드가 아니라, 파일 단위로 비교해 다른 것만 통째로 덮어쓴다. 같은 파일은 건드리지도 않는다. 로컬에서 직접 고친 파일은 묻거나 멈추고, chezmoi가 모르는 파일은 지우지 않는다(exact_ 모드가 아닌 한). 가장 안전한 기본값이다.
교훈
- 로컬 평문 자체는 정상이다. 진짜 위험은 git에 박제되는 것 — 그것만 암호화로 막으면 된다.
- age 동기화는
key.txt(열쇠) +chezmoi.toml(설정) 두 개가 새 Mac에 다 있어야 한다. 하나만 있으면encryption not configured. - 제외 목록이 추가 목록만큼 중요하다. 캐시·상태를 동기화하면 충돌난다. chezmoi는 명시한 것만 관리하니 콕 집어 넣자.
- 자동 동기화는 pull 전용 + 충돌 시 멈춤이 안전하다. 자동 push와 강제 덮어쓰기는 사고를 부른다.
- 그리고… 긴 명령은 한 줄로 복붙하자. 줄바꿈 하나가 반나절을 잡아먹는다.
이제 한 Mac에서 CLAUDE.md를 고치고 cz 한 번이면, 나머지 두 대가 알아서 따라온다. 목표 달성.