콘텐츠로 이동

Worktrees

문제

worktree를 열어놓고 실수로 원격에 올리거나, 병합할 때 처음 보는 에러로 막히거나, “정리됐다”는 말만 믿고 다음 단계로 넘어갔다가 상태가 꼬입니다.

추천

worktree는 열자마자 종료 계획까지 같이 세웁니다. 원격 접근은 여러 겹으로 막고, 실패는 커밋 전/후로 나눠서 읽고, 병합 순서가 꼬이는 함정(특히 exit-gate 문서)은 미리 피합니다.

이유

worktree는 열었으면 반드시 닫습니다. 진입과 종료를 짝으로 취급하고, 연 채로 방치하지 않습니다.

공유 체크아웃 편집은 첫 파일 편집에서 거부됩니다. 백그라운드 세션이 공유 체크아웃을 건드리면 읽기·검색은 막지 않지만 첫 Edit/Write에서 하니스가 거부합니다. 거부당한 뒤 격리하는 대신 처음부터 EnterWorktree로 시작합니다. 가드를 끄려면 .claude/settings.json에 "worktree": {"bgIsolation": "none"}을 둡니다.

worktree는 격리처럼 보이지만 저장소 안에 있습니다. 경로가 <repo>/.claude/worktrees/<name>이라 node 모듈 해석이 상위로 올라가 <repo>/node_modules의 실제 바이너리를 그대로 잡습니다. 로컬 node_modules를 지워도 격리되지 않는다는 뜻입니다. 파괴적인 CLI 래퍼를 테스트할 때 특히 위험합니다. 반대로 npm 스크립트를 돌릴 목적이라면 이 상위 탐색 덕분에 심링크 없이도 됩니다.

Git은 worktree 생성 당시 기준 브랜치를 보존하지 않습니다. main worktree의 현재 브랜치를 자동으로 원래 대상이라고 가정하면 안 됩니다.

원격을 못 건드리게 여러 겹으로 막습니다.

  1. 전역 정책에 worktree 결과는 원격에 push하거나 PR을 생성하지 않는다고 적습니다.
  2. PreToolUse 훅으로 push류 명령을 차단합니다(exit 2).
  3. 정리 절차 자체에도 “원격 브랜치는 변경하지 않는다”와 git worktree remove --force / git reset --hard / git clean 금지를 못박습니다.

가드는 한 번에 맞지 않습니다. push를 정규식으로 잡는 훅에서는 BSD sed(macOS)가 \b를 지원하지 않아 치환이 조용히 실행되지 않고 오탐이 남는 경우가 많습니다. 단어 경계를 다른 방식으로 흉내 낸 대안이 예상하지 못한 명령을 그대로 통과시키는 경우도 있습니다(예: 페이저 옵션을 낀 git ... push). 훅은 오탐을 낼 수 있다는 전제로 씁니다.

worktree 컨테이너 자체가 만드는 함정이 세 가지 더 있습니다.

  • 테스트가 옛 코드를 다시 돕니다. .claude/worktrees/<name>/는 git에는 무시되지만 테스트 러너 수집에는 무시되지 않습니다. squash 직후 main에서 다른 worktree에 남아 있던 옛 테스트 사본이 main의 alias 경로로 새 src를 상대로 돌아 거짓 실패를 낼 수 있습니다. 처방은 test.exclude에 **/.claude/worktrees/**를 등록하는 것입니다(빌드 시스템의 무시 목록과 같은 계열로 함께 등록합니다).
  • 원격 캐시가 죽으면 통과한 검증도 커밋에서 실패합니다. pre-commit이 영향 범위만 골라 도는 빌드 시스템(예: Nx의 affected)을 쓰는 레포에서 원격 캐시가 404를 내면 test/build가 통과해도 squash commit 자체가 exit 1로 죽습니다. 캐시를 건너뛰는 환경변수(Nx라면 NX_SKIP_NX_CACHE=true)를 머지 스크립트 앞에 붙이면 하위 프로세스로 전파돼 설정 파일을 손대지 않고 우회됩니다. 한 번 겪은 레포라면 다음부터 선적용합니다.
  • main이 항상 dirty해서 머지가 구조적으로 막힙니다. 머지 스크립트가 “main worktree에 미커밋 변경사항”으로 계속 막히고 범인이 ?? .claude/worktrees/ 한 줄이면, 그것은 사용자 파일이 아니라 worktree 컨테이너 자신입니다. worktree가 열려 있는 동안 main은 항상 dirty이고 dirty를 거부하는 스크립트는 계속 막힙니다. 근본 해결은 커밋되는 .gitignore에 .claude/worktrees/를 넣는 것입니다. 로컬 전용 exclude 파일에만 넣으면 다른 머신에서 재발합니다. 순서 함정이 하나 있습니다. 로컬 exclude 줄은 병합 후에 지웁니다.

병합 실패는 커밋 전/후로 나눠 읽습니다. 출력에 “squash commit이 생성됐지만”이 있으면 머지는 이미 끝났고 정리만 남았다는 뜻입니다. 이때 재실행하면 안 됩니다. “cleanup deferred”도 실패가 아닙니다. 먼저 세션 안에서 worktree를 유지한 채 빠져나옵니다. ExitWorktree의 keep 선택이고, remove가 아닙니다. 그다음 main에서 git worktree remove <경로>와 git branch -d <브랜치>로 정리합니다.

정리하기 전에 main과 source 브랜치의 트리가 같은지 git diff --stat main <source>로 확인합니다. target이 앞서 있어도 차이는 납니다. diff의 방향으로 진단합니다. 삭제만이면 안전합니다. 추가가 있으면 squash 누락입니다. 브랜치 쪽에 같은 커밋을 얹어 트리를 맞춘 뒤 정리합니다.

squash merge 충돌은 안전 실패입니다. main을 머지 전 상태로 되돌려 놓았다면 잃은 것이 없습니다. 해결은 main이 아니라 source에서 합니다. git merge main으로 흡수하고, 게이트를 재실행한 뒤, squash merge를 다시 시도합니다. 병렬 백그라운드 세션 환경에서는 분기 후 다른 세션이 main에 커밋하는 일이 흔합니다. 충돌 마커가 없는 파일에도 의미 드리프트가 생길 수 있으므로 병합 직후 상대 커밋의 stat을 훑습니다.

linked worktree에는 애초에 node_modules가 없습니다. git-tracked 파일만 체크아웃하기 때문입니다. 포맷터 하나 돌리는 정도면 main checkout의 바이너리를 빌리면 됩니다. 스크립트째 돌리는 명령(타입체크 등)을 쓰려면 main의 node_modules를 심링크하고 커밋 전에 반드시 지웁니다. 이 심링크는 .gitignore에 걸리지 않습니다. node_modules/처럼 뒤에 슬래시를 붙인 패턴은 디렉터리만 매치하고, git은 심링크를 파일로 봅니다.

lock 문구로 무엇을 쓸지 판단합니다. lock이 세션이 걸었다는 형태로 남아 있으면 그 lock은 EnterWorktree가 건 것이므로 remove -f -f가 아니라 ExitWorktree를 씁니다. 세션이 살아 있는데 git 레벨로 강제 제거하면 phantom-pin이 됩니다. 하니스가 이미 제거된 경로를 계속 활성으로 추적합니다. 복구는 편집 가드가 요구하는 경로에 worktree를 다시 만드는 것입니다.

main 기반 worktree는 feature branch 전용 파일을 통째로 놓칩니다. 복구는 git checkout <branch> -- <path> 또는 git show <branch>:<path> > <worktree경로>입니다. 예방은 git worktree add <path> <feature-branch>입니다. 생성 직후 git rev-parse HEAD로 기대 커밋인지 확인합니다.

exit-gate 문서(plan 체크박스·로드맵 상태)는 병합 전에 브랜치에서 끝내 단일 머지로 만듭니다. 병합 후 main에 따로 커밋하면 --no-ff 머지 커밋이 두 개 생깁니다. 되돌리려고 git reset --hard를 쓰면 요청 범위를 벗어난 파괴적 작업으로 판단해 차단하는 안전장치에 걸릴 수 있습니다.

예시

finish가 막힐 때 확인 순서:

터미널 창
git status # 범인이 `?? .claude/worktrees/` 한 줄뿐인가

.gitignore(커밋되는 쪽)에 넣어야 재발하지 않습니다.

.claude/worktrees/

정리 순서는 반드시 이 순서로:

터미널 창
# 1. 세션 안에서 worktree를 유지한 채 빠져나온다 (ExitWorktree의 keep. remove가 아니다)
# 2. main에서 머지가 끝났는지 확인한 뒤 정리한다
git diff --stat main <source-branch>
git worktree remove .claude/worktrees/<name>
git branch -d <source-branch>

원격 캐시가 죽어 있을 때 머지 스크립트 앞에 붙입니다(Nx 예시).

터미널 창
NX_SKIP_NX_CACHE=true <머지 스크립트>

하지 말 것

lock이 걸린 worktree를 git worktree remove -f -f로 강제 삭제하기. 세션이 살아 있으면 phantom-pin이 됩니다. ExitWorktree를 씁니다.

“squash commit이 생성됐지만” 메시지를 실패로 읽고 재실행하기. 머지는 이미 끝났습니다. 재실행하면 안 됩니다.

exit-gate 문서를 병합 후 main에 따로 커밋하기. --no-ff 머지 커밋이 두 개 생기고, 되돌리려는 git reset --hard가 안전장치에 막힐 수 있습니다. 병합 전 브랜치에서 끝내 단일 머지로 만듭니다.