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 cze='chezmoi edit --apply' # 의도적 편집: 소스 고치고 즉시 반영(권장 습관)
# 올리기 한방: re-add로 로컬 편집 자동 캡처(안전망) 후 commit+push
alias cz='chezmoi re-add && chezmoi git -- add -A && chezmoi git -- commit -m sync && chezmoi git -- push'- 고칠 땐
cze ~/.claude/CLAUDE.md— 소스를 편집하고 실제 파일에 즉시 반영까지 한 번에. 암호화 파일도 복호화→편집→재암호화 자동. - 올릴 땐
cz, 다른 Mac에서 받을 땐czu(또는 자동).
새 파일은 주의.
cz의re-add는 이미 관리 중인 파일의 변경만 캡처한다. 새 파일은chezmoi add <파일>(암호화면--encrypt)로 먼저 등록해야 소스로 들어간다.
chezmoi update는 파일을 지우고 다시 받나?
아니다. 디렉토리째 삭제 후 재다운로드가 아니라, 파일 단위로 비교해 다른 것만 통째로 덮어쓴다. 같은 파일은 건드리지도 않는다. 로컬에서 직접 고친 파일은 묻거나 멈추고, chezmoi가 모르는 파일은 지우지 않는다(exact_ 모드가 아닌 한). 가장 안전한 기본값이다.
셋업한 뒤 운영하며 밟은 지뢰 2개
설치만 끝나면 평화로울 줄 알았지만, 며칠 쓰면서 두 개를 더 밟았다. 둘 다 chezmoi 자체보다 **"동기화한다는 것의 의미"**를 다시 생각하게 한 지점이라 따로 적는다.
지뢰 1: 셸을 열 때마다 API 키가 터미널에 쏟아졌다
어느 날부터 새 터미널을 열거나 cz를 칠 때마다 환경변수가 화면에 와르르 출력됐다. CHEZMOI_* 부터 OPENAI_API_KEY 값까지 그대로. 등골이 서늘했다.
원인은 .zshrc의 깨진 한 줄이었다.
export
PATH="$HOME/.local/bin:$PATH"export와 PATH=... 사이에 줄바꿈이 끼어, 인자 없는 export가 단독으로 실행됐다. zsh에서 인자 없는 export는 **"모든 환경변수를 출력"**한다. 그래서 .zshenv가 export한 키들이 새 셸마다 화면에 찍힌 것이다. 한 줄로 붙이니 끝.
export PATH="$HOME/.local/bin:$PATH"교훈은 두 가지. 하나, dotfile에 숨은 미묘한 문법 버그는 동기화를 타고 3대에 똑같이 퍼진다. 둘, 그래서 한 곳에서 고쳐 push하면 세 대가 같이 고쳐진다 — 동기화의 양날을 모두 보여준 사건이었다.
지뢰 2: ~/.zshenv를 고치고 올렸는데 다른 Mac엔 옛 버전
이게 chezmoi의 진짜 핵심이다. 한 Mac에서 ~/.zshenv를 직접 고치고 cz(commit+push)까지 했는데, 다른 Mac에서 받아보니 예전 내용 그대로였다.
원인은 chezmoi의 모델을 오해한 데 있었다. 소스(encrypted_..._zshenv.age)가 진짜(source of truth)이고, ~/.zshenv는 거기서 생성된 결과물이다. 결과물을 직접 고쳐도 암호화 소스는 자동으로 갱신되지 않는다. cz(=git add)는 소스만 커밋하니, 소스가 그대로면 push할 것도 없다. 그래서 아무 일도 안 일어난 것이다.
해법은 결과물의 변경을 소스로 되담는 것이다.
chezmoi add --encrypt ~/.zshenv # 결과물 → 소스 재캡처(재암호화)여기서 워크플로우를 아예 사고 안 나게 바꿨다.
- 의도적으로 고칠 땐
cze(=chezmoi edit --apply) 로 소스를 직접 편집한다. 결과물에 즉시 반영되고, 항상 소스에 담겨 있다. drift가 없다. - 그래도 습관적으로 결과물을 직접 고쳤을 때를 대비해,
cz가 push 전에chezmoi re-add로 변경을 자동 캡처하게 했다.
즉 chezmoi에선 "파일을 고친다 = 소스를 고친다" 여야 한다. 이걸 몸에 익히는 게 전부다.
교훈
- 로컬 평문 자체는 정상이다. 진짜 위험은 git에 박제되는 것 — 그것만 암호화로 막으면 된다.
- age 동기화는
key.txt(열쇠) +chezmoi.toml(설정) 두 개가 새 Mac에 다 있어야 한다. 하나만 있으면encryption not configured. - 제외 목록이 추가 목록만큼 중요하다. 캐시·상태를 동기화하면 충돌난다. chezmoi는 명시한 것만 관리하니 콕 집어 넣자.
- 자동 동기화는 pull 전용 + 충돌 시 멈춤이 안전하다. 자동 push와 강제 덮어쓰기는 사고를 부른다.
- chezmoi에선 "파일을 고친다 = 소스를 고친다". 결과물을 직접 고쳤으면
chezmoi add로 되담고, 애초에chezmoi edit로 소스를 고치는 습관을 들이면 동기화 누락이 사라진다. - 그리고… 긴 명령은 한 줄로 복붙하자. 줄바꿈 하나가 반나절을 잡아먹는다 — 끊긴 명령도, 환경변수를 통째로 노출한 버그도 결국 줄바꿈에서 나왔다.
이제 한 Mac에서 CLAUDE.md를 고치고 cz 한 번이면, 나머지 두 대가 알아서 따라온다. 목표 달성.