Claude Code 설정을 여러 기기로 옮기기 — 통째 복사 대신 4분류
AI CLI 하네스(룰·훅·플러그인·시크릿)를 여러 기기에서 재현할 때, config·인증·캐시·시크릿을 어떻게 분리해야 조용히 안 깨지는지.
리눅스 데스크톱에서 Claude Code 하네스를 꽤 두껍게 만들어 뒀다. CLAUDE.md 룰,
훅, 스킬, 스테이터스라인, 플러그인, MCP 서버 선언까지. 이걸 맥북에서도 똑같이
쓰고 싶어서 ~/.claude 를 통째로 복사했다. 결과는 훅이 조용히 죽고, 스킬은 안
뜨고, MCP 는 인증이 꼬였다. 그때부터 “무엇을 옮기고 무엇을 옮기면 안 되는지”를
분류하는 게 이 작업의 전부라는 걸 알았다.
배경 — ~/.claude 는 세 가지가 섞여 있다
설정 디렉터리 하나 안에 성질이 완전히 다른 것들이 공존한다.
- 기기와 무관한 자산:
CLAUDE.md, 룰, 훅, 스크립트, 스킬,settings.json - 기기마다 새로 잡혀야 하는 것: 인증 토큰, 세션 상태, 캐시, 다운로드된 플러그인 바이너리
- 절대 평문으로 굴리면 안 되는 것: MCP API 키 같은 시크릿
통째 복사가 깨지는 이유가 여기 있다. 첫 번째는 그대로 옮겨야 맞지만, 두 번째를 같이 옮기면 다른 기기의 자격증명·머신 경로가 따라와서 깨지고, 세 번째는 git 에 올리는 순간 사고다.
증상·재현조건 — 통째 복사 뒤에 벌어지는 일
# 안티패턴
rsync -a ~/.claude/ new-machine:~/.claude/
이렇게 옮기면 겉보기엔 성공하지만 새 기기에서:
- 훅이 실행되다 말고 조용히 죽는다 (에러도 안 뜬다).
- 스킬 목록이 비거나 일부만 뜬다.
- MCP 서버가 “인증됨”으로 보이는데 호출하면 거부된다.
- 스테이터스라인이 안 그려진다.
공통점은 에러가 눈에 안 띈다는 것. 훅과 스테이터스라인은 실패해도 본 세션을 막지 않도록 설계돼 있어서, 깨진 걸 한참 뒤에야 안다.
실패했던 접근 — 시크릿을 1Password CLI 로 무인화
시크릿은 별도 금고에 두고 CLI 로 주입하려 했다. 개인용 요금제에서는 서비스 계정 토큰이 없어서 CLI 무인 인증이 안 된다는 게 문제였다.
eval "$(op signin)" # 세션 토큰이 env 로만 산다
AI 에이전트가 자율로 시크릿을 읽게 하려면 이게 치명적이다. 에이전트의 셸 호출은
매번 새 shell 이라 signin 으로 얻은 세션 토큰이 호출 간 남지 않는다. 매번
인터랙티브 로그인이 끼어들거나 세션이 만료된다. 장수 머신 토큰이 유일한 해법인데
그건 비즈니스 등급 전용이었다.
근본 원인 — “선언”과 “상태”는 다른 물건이다
플러그인·MCP·스킬이 기기 간에 “공유”되는 원리를 뜯어보면 답이 나온다. 공유되는 건 바이너리가 아니라 선언이다.
// settings.json — 이건 옮겨도 되는 선언
{
"enabledPlugins": { "some-plugin@marketplace": true },
"extraKnownMarketplaces": { "marketplace": { "source": { /* ... */ } } }
}
각 기기는 이 선언을 보고 마켓플레이스에서 자동으로 다시 설치한다. 캐시된 바이너리를 옮길 필요가 없다. 반대로 인증·세션·캐시는 그 기기에서 새로 만들어져야 정상이다. 그래서 분류가 이렇게 된다.
| 분류 | 예 | 처리 |
|---|---|---|
| A. 기기 무관 자산 | CLAUDE.md·룰·훅·스크립트·스킬·settings.json | git 커밋 |
| B. 플러그인·MCP | enabledPlugins / marketplace 선언 | 선언만 커밋 → 자동 재설치 |
| C. 인증·상태·캐시 | credentials·세션 json·projects·history | .gitignore, 커밋 금지 |
| S. 시크릿 | MCP API 키 | 암호화해서만 |
실제 해결 — sops + age 로 무인 복호
시크릿은 로컬 age 키파일로 복호하는 sops 로 갈았다. 인터랙티브 인증도 세션 만료도 아예 없어서 에이전트가 무인으로 읽고 쓸 수 있다. 암호화본 자체는 private repo 에 커밋되므로 git 동기화에 그대로 얹힌다.
# 최초 1회: age 키 생성 (공개키는 stderr 로 age1... 출력)
age-keygen -o "$HOME/.config/sops/age/keys.txt"
# .sops.yaml 의 creation_rules.age 에 그 공개키 등록
# 시크릿 편집 — 열면 자동 복호, 저장 시 자동 재암호화
sops secrets/secrets.enc.yaml
# 기기마다 복원: 복호 → 설정 json 에 병합 (평문을 디스크에 안 남긴다)
sops -d --output-type json secrets/secrets.enc.yaml | jq '/* merge */'
원칙 몇 가지가 이 구조를 지탱한다.
- age 개인키가 유일한 마스터. git 금지(
.gitignore), 기기당 1회 수동 복사. 분실 대비 콜드백업은 별도 금고에 한 번만. - 정적 API 키만 암호화 대상. OAuth 로 도는 MCP 는 저장하지 않고 기기마다
/mcp에서 재인증한다. OAuth 토큰은 애초에 이식 대상이 아니다. - 기기/키 추가 시
.sops.yaml에 공개키를 넣고sops updatekeys로 재봉인.
검증 방법 — 커밋 전 스캔과 배포 후 확인
repo 를 만들 때 .gitignore 는 최후의 안전망으로만 둔다. 실질 방어는 커밋 전
시크릿 스캔이다.
# 흔한 토큰 형태를 통으로 훑는다
grep -rEi 'Bearer [A-Za-z0-9._-]{8,}|[A-Fa-f0-9]{32,}' .
# 무인 복호가 되는지 (인증 절차 0 이어야 정상)
sops -d secrets/secrets.enc.yaml >/dev/null && echo ok
# 배포 후: 설정에 머신 경로가 새지 않았는지
grep -rn '/home/\|/Users/' ~/.claude/settings.json ~/.claude/statusline.sh \
|| echo "머신 경로 없음"
# 훅이 가리키는 경로가 전부 실재하는지 전수 확인
jq -r '.hooks|to_entries[]|.value[]|.hooks[]|.command' ~/.claude/settings.json
설정과 훅은 새 세션부터 로드된다. 배포 뒤에는 CLI 를 반드시 재기동한다.
타환경 주의점 — 조용히 실패하는 지점들
node 절대경로 함정. 훅과 스테이터스라인이 특정 node 경로를 박아 두면 위험하다.
fnm/nvm 의 which node 는 셸 수명짜리 임시 심링크를 준다.
/run/user/1000/fnm_multishells/<pid>_<ts>/bin/node
이걸 그대로 박으면 그 셸이 죽는 순간 훅이 조용히 죽는다. 안정 경로로 한 번 고정해 두는 게 답이다.
mkdir -p ~/.local/bin
ln -sf "$(readlink -f "$(which node)")" ~/.local/bin/node # readlink -f 가 핵심
settings.json 을 통으로 덮으면 live 전용 플러그인 등록이 날아간다. GUI 로
설치한 플러그인은 live 의 enabledPlugins / extraKnownMarketplaces 에만 있고
repo 는 모른다. 그대로 cp 하면 그 플러그인이 빠진다. 이 두 키만 live 값으로
병합해서 배포한다.
jq -s --argjson keys '["enabledPlugins","extraKnownMarketplaces"]' \
'.[0] as $repo | .[1] as $live
| reduce $keys[] as $k ($repo;
if ($live[$k] // null) == null then .
else .[$k] = ($live[$k]
| if type=="object" then to_entries|sort_by(.key)|from_entries else . end)
end)' \
settings.json ~/.claude/settings.json > /tmp/settings.merged.json
sort_by(.key) 가 있어야 기기별 설치 순서 차이로 내용이 같은데도 매번 diff 가
나는 flip-flop 커밋을 막는다.
플랫폼별 경로/명령 차이. OS 알림 훅처럼 플랫폼 전용 명령(리눅스 ↔ macOS)은
기기마다 갈아 끼워야 한다. 특히 macOS sed 는 in-place 편집에 빈 인자를 요구한다.
sed -i '' 's/old/new/' file # macOS (BSD sed)
sed -i 's/old/new/' file # Linux (GNU sed)
스킬은 진입 파일이 있어야 로드된다. 스킬 폴더에 진입 매니페스트(예: SKILL.md)가
없으면 로드되지 않고 그냥 스캐폴드 폴더로 남는다. 안 뜨는 스킬이 있으면 파일을
빠뜨린 게 아니라 진입점이 없는 것부터 의심한다.