나는 머신 이전에서 살아남는 법에 관한 글을 발행했다. 일주일 뒤, 내 설정이 이미 실패해 있었다는 걸 발견했다, 그리고 그걸 고치는 데는 git, 심링크, launchd, 그리고 겸허해지는 오후 한 나절이 필요했다.
일주일 전, 나는 워크스페이스 아키텍처에 관한 글을 발행했다.
논지는 자신만만했다.
오늘 내 머신이 죽는다면, 디렉토리 하나에서 모든 걸 재구축할 수 있어야 한다.
~/Developer/ 아래 모든 것. 저장소들. 인프라. 데이터. 디렉토리 하나, 완전히 이식 가능.
꽤 뿌듯했다.
그러다 며칠 뒤, 나는 내 AI 어시스턴트에게 Claude Code 스킬 폴더를 새 구조로 백업해달라고 요청했다.
돌아온 답은 예상 밖이었다.
ls: /Users/paurushrai/.claude/skills/: No such file or directory그 폴더는 존재하지 않았다.
하지만 나는 분명 스킬이 있었다. graphify라는 커스텀 스킬, 내가 만들었고, 매일 썼고, 내 전역 설정은 여전히 그걸 참조하고 있었다. 그럼 그건 어디 있었나?
/Users/paurushrai/Downloads/mac-restore/dot-claude/skills/graphify/거기 있었다. 내 Downloads 폴더 안. mac-restore라는 디렉토리 안에.
머신을 이전한 그날부터 계속 거기 앉아 있었다. 한 번도 복원되지 않은 채. 잊힌 채.

나는 정확히 이 실패 양상에 관한 글 한 편을 통째로 써놓고, 정작 내 머신은 조용히 그걸 겪고 있었다.
🔍 사각지대
내가 놓쳤던 게 뭔지 짚어보자.
내 아키텍처는 모든 것을 ~/Developer/ 안에 조직했다. 하지만 머신을 내 것처럼 느끼게 만드는 설정은 거기 살지 않는다.
- git 아이덴티티와 동작,
~/.gitconfig - 셸 설정,
~/.zshrc,~/.zprofile - 전역 ignore,
~/.gitignore_global - AI 코딩 규칙,
~/.claude/CLAUDE.md - 커스텀 AI 스킬,
~/.claude/skills/ - 그리고 가장 눈에 안 띄는 것: 설치된 패키지 목록. 이건 어디에도 살지 않는다
이 모든 것이 $HOME 전역에 흩어져 있었다. 버전 관리도 안 되고, 백업도 안 되고, 내 아름다운 디렉토리 구조에는 보이지도 않았다.
피해를 가늠하려고 ~/.gitconfig를 확인했을 때, 생각보다 더 심각했다.
cat ~/.gitconfig
# → no such file그 파일은 존재하지 않았다. 이전 머신에서는 이메일 라우팅을 설정해뒀었다, 내 프로젝트엔 개인 이메일, 회사 저장소엔 업무 이메일. 그중 어느 것도 살아남지 못했다. 나는 몇 달 동안 그걸 눈치도 못 챈 채 커밋하고 있었다.
코드는 이전됐다. 설정은 그러지 못했다.
🔗 유닉스에서 가장 오래된 트릭
내 첫 본능은 뻔한 것이었다. 백업 폴더를 만들고, dotfile들을 거기 복사하고, GitHub에 푸시한다.
하지만 사본은 어긋난다. .gitconfig의 버전이 두 개가 되는 순간, 그중 하나는 거짓말이 된다, 그리고 뭔가 망가지기 전까진 어느 쪽인지 알 수 없다.
해결책은 나보다도 오래됐다. 심링크다.
파일은 하나만 존재한다. 저장소 안에 산다. 홈 디렉토리는 포인터만 가진다.
~/.gitconfig ──symlink──▶ ~/Developer/infra/dotfiles/.gitconfig
└── 유일한 실제 파일
이게 결국 다시 동기화하고 다닐 필요가 있다는 뜻인지 물었을 때, 이 설계 전체를 작동시키는 답을 얻었다.
쓰기는 심링크를 통과한다.
git config --global을 실행해서 설정을 바꾸면, git은 포인터를 따라가서 저장소 파일에 직접 쓴다. ~/.zshrc를 편집하면, 저장소 파일을 편집하는 것이다. 동기화할 게 없다. 애초에 동기화할 대상이 없으니까. 파일 하나, 주소 두 개.
그래서 이렇게 만들었다. 내 인프라 계층 안에 dotfiles 저장소를 두고, 실제 파일들을 담고, 새 머신에서 모든 심링크를 재생성하는 멱등적인 install.sh를 함께 뒀다, 먼저 발견한 건 뭐든 백업하고, 두 번 실행해도 안전하게.
git 설정, 셸 설정, 전역 ignore, 내 AI 규칙, 구조된 graphify 스킬까지. 커밋됐다. 푸시됐다. 영구적이다.
저장소에 들어갈 뻔한 것을 살펴보다가 배운 규칙 하나. 절대 비밀 정보는 커밋하지 않는다. 내 .zshrc는 이제 git이 절대 보지 못하는 ~/.zshrc.local을 source한다. 저장소는 구조를 갖고, 머신은 비밀을 지킨다.

끝났다고 생각했다.
🍺 Homebrew는 일기를 쓰지 않는다
그러고 나서 간단한 질문 하나에 답해보려 했다. 이 머신이 죽으면, 나는 뭘 다시 설치해야 하나?
git 설정에는 아름다운 속성이 있다. 도구 자체가 자기 설정 파일에 쓰기 때문에, 저장소는 항상 진실을 안다. Homebrew에는 그런 예의가 없다. brew install jq는 패키지를 설치하고 그 결정을 어디에도 기록하지 않는다. 패키지 목록은 오직 암묵적으로만 존재한다. /opt/homebrew/Cellar 안의 폴더들로.
Homebrew는 요청하면 매니페스트를 덤프해줄 수 있다.
brew bundle dump --global --force
# → .Brewfile 작성: brew "gh", brew "git", brew "tree"...그래서 다른 것들처럼 .Brewfile도 저장소에 심링크했다. 하지만 함정이 하나 있었고, 이게 계속 신경 쓰였다.
Brewfile은 덤프하는 걸 기억할 때만 업데이트된다.
내 설계 전체는 내가 기억하지 못한다는 전제 위에 세워져 있었다. 그게 애초에 심링크가 존재하는 이유 전부다. 내 기억력에 의존하는 매니페스트는, 필요할 즈음이면 이미 석 달은 묵어 있을 매니페스트다.
🧪 첫 시도는 틀렸다
첫 해결책은 영리하게 느껴졌다. brew 명령어를 셸 함수로 감싸는 것. 모든 install과 uninstall을 가로채서, 매니페스트를 다시 덤프하고, 자동 커밋하고, 자동 푸시한다.
만들었다. 첫 시도에 작동했다.
$ brew install jq
...
$ git log --oneline -1
df053cb chore(brew): install jq훌륭했다. 커밋 메시지가 패키지 이름까지 담고 있었다.
그리고 다시 보다가 구멍을 발견했다.
셸 래퍼는 내 인터랙티브 셸에 입력된 명령어만 잡아낸다. 의존성을 설치하는 셋업 스크립트? 안 보인다. 세션 안에서 brew를 실행하는 AI 에이전트? 안 보인다. 내 설정이 로드된 zsh가 아닌 어떤 컨텍스트든? 안 보인다.
나는 이벤트를 자동화해야 했는데 인터페이스를 자동화했던 것이다.
패키지 설치 그 자체가 이벤트다, 그리고 그건 내 셸이 아니라 파일시스템에서 일어난다.
⚙️ OS는 이미 답을 갖고 있었다
물었다. launchd를 이걸 위해 쓸 수 있을까?
launchd는 macOS의 서비스 관리자다, 시스템의 모든 데몬을 시작하는 그것, Apple 버전의 systemd다. 모든 맥 개발자가 본 적은 있다. 그런데 거의 아무도 최고의 기능은 쓰지 않는다.
WatchPaths, launchd는 OS 레벨에서 경로를 감시하다가, 바뀌는 순간 작업을 실행한다.
<key>WatchPaths</key>
<array>
<string>/opt/homebrew/Cellar</string>
<string>/opt/homebrew/Caskroom</string>
</array>이제 누가 패키지를 설치하는지는 중요하지 않다, 내 셸이든, 스크립트든, 에이전트든, 무엇이든. Cellar가 바뀌고, OS가 알아채고, 내 동기화 스크립트가 실행된다.
# sync.sh, 엔진의 전부
brew bundle dump --global --force # 패키지 스냅샷
git add -A # dotfile 변경 사항 스테이징
git diff --cached --quiet && exit 0 # 아무것도 안 바뀌었으면 조용히 종료
git commit -q -m "chore: auto-sync ${CHANGED}"
git push -q # 오프라인이면 로컬에 커밋, 다음번에 푸시거기에 안전장치로 정오마다 한 번씩 실행되고, 60초 스로틀을 둬서 패키지 다섯 개를 설치해도 커밋이 다섯 개가 아니라 하나가 되게 했다.
그러고 나서 테스트. 패키지 하나 설치하고, 아무것도 건드리지 않고, 지켜본다.
$ brew install jq
... 8초 뒤, 셸 개입 전혀 없이:
67ba27e chore: auto-sync .Brewfile
→ GitHub로 푸시됨 ✅8초. 머신이 스스로 변경 사항을 문서화하고, 커밋하고, 내가 앉아 있는 사이에 푸시했다.

셸 래퍼는 그 순간 바로 지웠다. 서드파티 도구도, 타이머로 폴링하는 cron도, 설치할 데몬도 없다. OS가 처음부터 답을 갖고 있었다.
그게 패키지에 통하자, 다음 단계는 뻔했다. dotfile 자체를 감시하는 것. 이제 .zshrc나 내 AI 규칙을 편집하면 1분 안에 자동 커밋되고, 파일명이 커밋 메시지에 담긴다.
🔁 하마터면 만들 뻔했던 함정
여기서 결정 하나가 이걸 만드는 누구든 물어뜯을 것이니, 고백할 가치가 있다.
게으른 버전은 저장소의 루트 디렉토리를 감시하는 것이다. 감시 항목 하나로 모든 걸 커버한다.
하지만 모든 git 커밋은 .git/을 변경한다. 저장소 루트를 감시하면 우로보로스를 만든 셈이다. 변경이 에이전트를 발동시킨다 → 에이전트가 커밋한다 → 그 커밋이 .git/을 바꾼다 → 그게 다시 에이전트를 발동시킨다 → 영원히.
그래서 이 에이전트는 개별 파일을 감시하지, .git을 담은 디렉토리는 절대 감시하지 않는다. 트레이드오프는 이렇다. 깊은 서브디렉토리 안의 새 파일은 즉시 잡히지 않는다. 대신 정오 실행이 쓸어담는다. 그건 README에 문서화돼 있고, 공정한 대가다.
스스로를 발동시키는 자동화는 새벽 2시에 발견하게 되는 종류의 버그다.
🔔 말하게 가르치기
남은 문제가 하나 있었고, 그건 신뢰에 관한 것이었다.
이 에이전트는 이제 내 대신 조용히 커밋을 하고 있었다. 조용한 데몬은 신뢰를 잃는 데몬이다, 아니면 더 나쁘게, 존재 자체를 잊어버리는 데몬이다.
스크립트에 macOS 네이티브 알림 두 줄을 넣었다.
osascript -e 'display notification "Committed & pushed: .Brewfile" with title "dotfiles sync"'첫 테스트: 나는 그걸 보지 못했다. 배너는 5초 만에 사라졌고, 나는 다른 곳을 보고 있었다.
해결책은 코드가 아니라 설정 하나였다. macOS 알림에는 두 가지 스타일이 있고, 거의 아무도 기본값을 바꾸지 않는다. 시스템 설정 → 알림 → Script Editor → 알림. 알림(Alert) 스타일은 직접 닫을 때까지 화면에 남는다.
이제 모든 동기화는 내가 확인해줄 때까지 구석에서 기다린다, 바뀐 파일명과 함께. 내 머신의 설정 히스토리가, 영수증으로 배달된다.
그리고 실제로 뭔가 바뀌었을 때만 말한다, 아무 일도 없던 동기화는 조용하다. 1분마다 울리는 알림은 이미 무시하는 법을 배운 알림이다.
🎭 편집증적인 질문
모든 게 작동하던 바로 그때, 대부분의 이런 설정을 무너뜨리는 질문이 떠올랐다.
GitHub에 내 업무 계정으로 로그인하면 무슨 일이 일어나지?
찾아보니, "GitHub에 로그인했다"는 건 사실 대부분의 사람이 한 번도 분리해본 적 없는 세 개의 독립된 레이어였다.
레이어 1, 누가 커밋을 작성했나? git 설정이 전적으로 결정한다. 끝. 커밋은 로컬 파일 쓰기이고, GitHub에는 연락조차 가지 않는다. 내 includeIf 규칙은 회사 저장소를 업무 이메일로, 나머지 전부를 개인 이메일로 라우팅한다. CLI 로그인은 이걸 건드릴 수 없다.
레이어 2, 누가 푸시할 권한이 있나? macOS 키체인에 든 자격 증명이 결정한다. 그리고 여기가 취약한 부분이다. 키체인은 자격 증명을 호스트별로 저장한다. 계정 두 개, github.com 하나, 두 번째 로그인이 첫 번째를 조용히 덮어쓸 수 있고, 갑자기 내 동기화 에이전트가 잘못된 아이덴티티로 푸시하며 403을 모으게 된다.
레이어 3, gh CLI는 누구로 행동하나? 활성화된 계정이 뭐든. 순수하게 git만 다루는 내 동기화와는 무관하다.

레이어 2가 진짜 노출 지점이었고, 해결책은 한 줄이다.
git remote set-url origin https://paurushrai@github.com/paurushrai/dotfiles.git그 paurushrai@는 이메일이 아니다. ssh와 같은 user@host라는 URL 문법이다. URL에 사용자명이 있으면, 키체인은 자격 증명을 호스트 + 사용자명으로 정리해서 저장한다. 별도 계정, 별도 서랍, 덮어쓸 일 없음.
이 원칙은 이메일 라우팅 뒤에 있는 것과 같은 원칙이고, 이렇게 정리할 만하다.
아이덴티티는 저장소의 속성이어야 한다, 전역의 가변 상태의 속성이 아니라.
🚀 10분짜리 머신
여기까지가 이 이야기의 결말이다.
다음에 맥을 새로 개봉할 때, 전체 절차는 이렇다.
# 1. Homebrew, 그다음 두 개의 부트스트랩 도구
brew install gh git
gh auth login# 2. 설정을 클론하고, 모든 걸 링크하고, 모든 패키지를 재설치
gh repo clone paurushrai/dotfiles ~/Developer/infra/dotfiles
~/Developer/infra/dotfiles/install.sh
brew bundle --global# 3. 재인증, 자격 증명은 설계상 이전되지 않는다
claude login그리고 내가 진심으로 만족스러운 부분. install.sh는 설정만 복원하지 않는다. launchd 에이전트까지 부트스트랩한다.
새 머신은 내 설정의 스냅샷을 받는 게 아니다. 내 설정을 유지하는 시스템을 받고, 1분 차부터 스스로를 유지하기 시작한다.
의도적으로 빼놓은 것들도 그만큼 중요하다.
- 자격 증명, 비밀의 백업은 다시 로그인하는 것이다. 10초, 의도적으로.
- 캐시, 세션 상태, 빌드 산출물, 자연스럽게 재구축된다. 이런 걸 백업하는 건 쌓아두는 것이다.
- 데이터 계층, Postgres와 Redis 볼륨은 크고, 살아있고, 계속 변하는 바이너리다. git은 잘못된 도구고, 실행 중인 데이터베이스를 복사하면 손상된 쓰레기가 나온다. 데이터는 진짜 스냅샷 백업이 필요하다, 그건 다음 글이다.
🎯 내가 실제로 배운 것
지난 글의 교훈은 이거였다. 당신의 워크스테이션은 시스템이다, 설계하라.
이번 주는 그 따름 정리를 가르쳐줬다. 규율로 유지되는 시스템은 부패한다. 스스로를 유지하는 시스템은 복리로 쌓인다.
이 이야기 속 모든 해결책의 모양을 보라.
- 나는 설정 파일 복사를 잊을 것이었다 → 그래서 파일은 하나뿐이고, 심링크되어 있고, 사본이 없다
- 나는 패키지 매니페스트 업데이트를 잊을 것이었다 → 그래서 OS가 파일시스템을 감시한다
- 나는 커밋을 잊을 것이었다 → 그래서 에이전트가 커밋한다
- 나는 조용한 실패를 놓칠 것이었다 → 그래서 알림은 내가 닫을 때까지 기다린다
- 나는 다음번에 이 모든 걸 다시 설정하는 걸 잊을 것이었다 → 그래서 설치 스크립트가 자동화 자체를 재설치한다
하나하나가 "기억해야 함"을 "저절로 됨"으로 바꿔놓는다.
그 스킬 파일은 그게 존재한다는 걸 내가 기억하는 데 복원이 달려 있었기 때문에 몇 달째 Downloads 폴더에서 죽어 있었다. 이제 내 머신의 어떤 것도 내 기억력에 의존하지 않는다.
그리고 솔직히, 그게 내가 나 자신에게 출시한 최고의 기능이다.
당신의 설정 계층은 어떻게 생겼나
Dotfiles 저장소는 오래된 아이디어다. 하지만 대부분은 "저장소 안의 파일들 + install 스크립트"에서 멈춘다. 첫날 이후에도 저장소를 진실되게 유지하는 부분이야말로 흥미로워지는 지점이다.
툴링은 계속 바뀔 것이다. 아키텍처는 그걸 견뎌내야 한다.


