Claude Code 설정을 기기 간에 동기화하는 플러그인, claude-sync
어느 기기에서 열어도 같은 Claude로
2026/09/02Claude Code 설정을 기기 간에 동기화하는 플러그인, claude-sync
Claude Code를 쓰다 보면 자연스레 설정이 쌓인다. 직접 만든 에이전트와 스킬, 모든 프로젝트에 공통으로 쓸 CLAUDE.md, 추가로 설치한 플러그인과 MCP 서버까지. 문제는 이 모든 게 기기 로컬에만 머문다는 점이다. 여러 기기에서 Claude Code를 쓰다 보니, 한쪽에서 공들여 다듬은 설정을 다른 기기로 일일이 복사하는 과정이 퍽 번거로웠다.
dotfiles처럼 Git 저장소에 올려두면 될 것 같지만, 그대로 올리기엔 난감한 부분들이 있다. MCP 서버 설정에 API 키 같은 민감한 정보가 포함되어 있고, 여러 기기에서 수정한 설정을 충돌 없이 합치는 과정도 필요하기 때문이다. 그래서 이 귀찮은 작업들을 대신 처리해 줄 Claude Code 플러그인, claude-sync를 직접 만들었다.
무엇을 동기화하나
| 로컬 | 레포에 저장되는 형태 |
|---|---|
~/.claude/agents/ |
커스텀 에이전트 파일 그대로 |
~/.claude/skills/ |
스킬 파일 그대로 |
~/.claude/CLAUDE.md |
그대로 |
~/.claude/settings.json |
plugins.json — 플러그인 목록, 마켓플레이스, 플러그인 설정의 키 이름 |
~/.claude.json의 user 스코프 MCP 서버 |
mcp-servers.json |
settings.json은 원본을 통째로 올리지 않고 플러그인 동기화에 필요한 필드만 추출해서 올린다. MCP 서버 역시 ~/.claude.json 최상위에 있는 user 스코프 서버만 대상이다. claude.ai 계정 커넥터나 플러그인이 자체 제공하는 서버, 프로젝트·로컬 스코프 서버 등은 자연스럽게 제외된다.
지원하는 명령어(스킬)는 세 가지다.
| 명령어 | 하는 일 |
|---|---|
/sync-backup |
로컬 설정을 레포에 백업하고 push |
/sync-restore |
레포에서 설정을 복원 |
/sync-status |
로컬과 레포의 차이만 확인 (dry-run) |
설치와 사용
claude plugin marketplace add claude-sync --source github --repo june20516/claude-sync
claude plugin install claude-sync@claude-sync
설정이 세팅된 기기에서 /sync-backup을 실행하면 된다. 처음 실행 시 백업용 Git 저장소 URL을 입력받으며, 이후부터는 해당 저장소를 계속 사용한다. 새로운 기기에서는 똑같이 플러그인을 설치한 뒤 /sync-restore만 실행하면 설정이 그대로 복원된다.
아직 Claude Code조차 설치하지 않은 새 환경이라면, 백업 저장소를 클론한 뒤 bootstrap.sh 스크립트를 실행해 한 번에 복원할 수도 있다.
git clone <백업-레포-url> /tmp/claude-sync-repo
bash /tmp/claude-sync-repo/bootstrap.sh
CLAUDE.md나 에이전트 파일에는 사내 인트라넷 URL 같은 민감한 정보가 섞여 들어가기 쉽다. 따라서 백업 저장소는 반드시 private으로 설정할 것을 권장한다. 특정 파일을 동기화에서 제외하고 싶다면 ~/.claude/.syncignore 파일에 glob 패턴으로 명시하면 된다.
Secret 값 처리
MCP 서버 설정에는 인증 토큰이나 비밀번호 같은 민감한 값이 들어간다. 예를 들어 ~/.claude.json에 다음과 같은 두 서버가 있다고 가정해 보자.
{
"mcpServers": {
"docs-search": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer sk-live-1234" }
},
"local-db": {
"type": "stdio",
"command": "npx",
"args": ["-y", "some-db-mcp"],
"env": { "DB_PASSWORD": "hunter2" }
}
}
}
이를 백업하면 저장소의 mcp-servers.json에는 다음과 같이 올라간다.
{
"scope": "user",
"servers": {
"docs-search": {
"headers": { "Authorization": "<REDACTED>" },
"type": "http",
"url": "https://mcp.example.com/mcp"
},
"local-db": {
"args": ["-y", "some-db-mcp"],
"command": "npx",
"env": { "DB_PASSWORD": "<REDACTED>" },
"type": "stdio"
}
},
"version": 2
}
보안이 필요한 headers와 env의 값만 <REDACTED>로 마스킹하고, 키(Key) 이름은 남긴다. 키 자체를 지워버리면 새 기기에서 복원할 때 이 서버 구동에 어떤 값이 필요한지 알 길이 없기 때문이다. 키 이름이 남아있기 때문에 /sync-restore 실행 시 docs-search 서버에 Authorization 값이 필요하다고 물어볼 수 있으며, 입력을 건너뛰면 해당 서버는 등록되지 않는다. 플러그인 설정 값도 동일한 방식으로 처리된다.
단, 마스킹 대상은 headers와 env에 한정된다. args나 URL 쿼리스트링에 직접 키를 하드코딩해 둔 서버라면 그 값은 마스킹 없이 그대로 올라가니 주의해야 한다.
판정은 스크립트가, 선택은 대화로
동기화 과정에서 가장 까다로운 부분은 '누가 무엇을 바꿨는지' 판단하는 일이다. claude-sync는 단순한 파일 수정 시각에 의존하지 않고, 파일 내용의 해시값을 기반으로 3-way 비교를 수행한다. 기기마다 마지막으로 저장소와 동기화했던 상태(base)를 기억해 두고, 이를 현재의 로컬 및 저장소 상태와 대조하는 방식이다.
- 한쪽만 변경되었다면 변경된 쪽을 따른다.
- 양쪽 모두 변경되었다면
git merge-file을 이용해 병합을 시도하며, 동일한 줄에서 충돌이 발생했을 때만 충돌 상태로 남긴다. 이때 로컬 파일 원본은 건드리지 않는다. - MCP 서버와 플러그인은 파일 전체를 덮어쓰는 대신, 서버 이름과 항목 단위로 따로 판정한다. 특정 기기에만 있는 서버가 다른 기기에서 백업을 진행했다고 해서 지워지는 것을 막기 위해서다.
이러한 복잡한 판정은 모두 내부 스크립트가 전담하며, 스킬 문서(SKILL.md)는 그 결과를 사용자에게 전하고 의견을 묻는 역할만 한다. 세 개의 스킬이 각자 독립적인 판정 로직을 가지면 결과가 엇갈리기 쉽기 때문이다. 실제로 이전 버전(v2)에서는 /sync-status와 /sync-backup이 MCP 목록을 파싱하는 정규식이 미묘하게 달라, 직전에 백업을 마쳤는데도 상태 확인 시 차이가 있다고 보고하는 문제가 있었다.
스크립트가 스스로 결정할 수 없는 모호한 상황에서만 사람에게 묻는다. 다른 기기에서 지운 서버를 이 기기에서도 지울지, 양쪽에서 다르게 수정한 설정을 어느 쪽 기준으로 맞출지 등이다. /sync-restore는 이런 항목들을 대화형으로 하나씩 짚어가며 해결한다. 또한 복원은 저장소의 데이터를 가져오기만 할 뿐, 로컬에서 바뀐 내용을 알아서 레포에 올리지는 않도록 역할을 분리했다.
만들면서 겪은 일
판정표를 다 덮은 테스트가 통과했는데, 서버가 사라졌다
v3에서 MCP 서버를 이름 단위로 병합하도록 다시 설계하면서, 처음에는 "백업이 성공하면 base를 레포 파일 전체로 갱신한다" 는 규칙을 세웠다. 얼핏 자연스럽지만, 여기엔 치명적인 함정이 있었다.
- 새 기기에서 백업을 한다. 저장소에는 다른 기기가 올린 서버
X가 있고, 이 기기에는 없다. 이번 백업은X를 저장소에 그대로 둔다. 여기까지는 맞다. - 백업이 성공했으므로 base를 저장소 파일 전체로 갱신한다. 결과적으로 이 기기가 받아온 적도 없는
X가 base에 포함된다. - 복원 없이 한 번 더 백업한다. 이번엔 base에
X가 있는데 로컬에는 없으니 "이 기기가X를 지웠다" 로 해석된다. 결국 저장소에서X가 삭제된다.
새 기기에서 복원 없이 백업을 연달아 두 번 실행하면 다른 기기의 서버가 경고 없이 날아가는 버그였다. 이 결함은 판정표의 모든 경우의 수를 커버한 테스트 코드를 무사히 통과했다. 테스트 시나리오가 모두 백업을 단 한 번만 호출하도록 작성되었기 때문이다. 한 번의 판정은 정확했지만, 그 판정이 남긴 base가 다음 판정을 망가뜨리는 흐름을 놓친 것이다. 다행히 실제 배포 전 코드 리뷰의 시뮬레이션 과정에서 결함을 잡아낼 수 있었다.
수정된 규칙은 "base는 로컬이 그 상태 변화에 동의(반영)했을 때만 전진한다" 이다. 그리고 동일한 입력으로 백업을 여러 번 반복하더라도 결과가 흔들림 없이 한곳에 머무는지(고정점) 검증하는 테스트를 추가했다.
테스트는 통과했는데, 실제 기기에서 조용히 실패했다
v3.1.0을 실제 기기에서 구동해 보며 발견한 문제다. /sync-backup의 특정 단계에서 갱신할 파일 목록을 모으는 bash 코드가 있었다.
mapfile -t BASE_RELS < <(python3 -c "...")
mapfile은 bash 4.0 이상에서 추가된 빌트인 명령어라 macOS의 기본 bash(3.2)나 zsh에서는 존재하지 않는다. 해당 환경에서는 이 줄이 command not found 에러와 함께 실패하고 배열은 빈 채로 남는다. 이어지는 조건문이 빈 배열을 "갱신할 내용 없음"으로 인식하니, base를 갱신하는 스크립트는 아예 호출조차 되지 않았다. 에러나 경고 메시지도 없고 종료 코드는 0이었기에 겉보기엔 백업이 정상적으로 완료된 것처럼 끝났다.
테스트 코드가 없었던 것은 아니다. 다만 그 테스트는 SKILL.md에 작성된 bash 블록의 텍스트(문자열) 자체만 검사했을 뿐, 실제 셸 환경에서 구동해 보지 않은 것이 문제였다. v3.1.1에서 이를 while read 구문으로 교체하고, 해당 블록을 bash와 zsh 환경에서 실제로 실행하여 검증하는 테스트를 추가했다.
BASE_RELS=()
while IFS= read -r rel; do
[ -n "$rel" ] && BASE_RELS+=("$rel")
done < <(python3 -c "...")
그런데 코드를 수정하며 달아둔 "while read 구문은 세 가지 셸에서 모두 작동한다"는 주석조차 거짓이었다. POSIX sh 환경에서는 프로세스 치환(< <(...)) 문법을 지원하지 않기 때문이다. v3.1.2에서 이 주석을 바로잡고, 주석이 주장하는 타겟 셸과 테스트가 실제로 실행되는 셸 환경이 일치하는지 확인하도록 수정했다.
두 장애 모두 원인은 같았다. 코드가 어떻게 동작할 것이라는 주장이 실제 측정(테스트 환경)과 단단히 결합되어 있지 않았던 것이다.
마무리
설정 동기화는 파일 몇 개를 복사하는 단순한 일처럼 보였지만, 막상 구현해 보니 '누가 언제 무엇을 바꿨는지'를 엄밀하게 추적하는 상태 기계(State Machine)에 가까웠다. 여러 기기를 오가며 Claude Code를 사용하는 분들이라면 한 번쯤 써 보셔도 좋겠다.
이 플러그인을 어떤 테스트 전략으로 개발했는지는 다음 글에 자세히 정리했다. 설치 명령어와 저장소 링크는 Lab의 Claude Tools 페이지에도 모아두었다.