왜 세션을 여러 개 띄우게 됐나
혼자서 CodeIgniter 4 기반 솔루션 여러 개를 동시에 굴리다 보면, Claude Code 세션 하나로는 부족한 순간이 옵니다. 한 쪽에서는 새 기능을 만들고, 다른 쪽에서는 버그를 잡고, 또 다른 쪽에서는 테스트를 보강하는 식입니다.
문제는 같은 저장소 디렉터리에서 세션을 두 개 띄우면 둘이 같은 파일을 동시에 고친다는 점입니다. 그래서 git worktree로 세션마다 체크아웃을 따로 주는데, 여기서 두 번째 함정을 만납니다. worktree는 파일은 나눠 주지만 저장소(.git)는 하나를 같이 씁니다.
이 글은 그 ‘같이 쓰는 부분’ 때문에 생기는 사고 두 가지(브랜치 충돌, stash 공유)와, 제가 지키는 운영 규칙을 정리한 것입니다. 기능 소개보다는 “어디서 터지는가”에 집중했습니다.
worktree가 나눠 주는 것, 같이 쓰는 것
사고는 거의 항상 “당연히 따로일 줄 알았는데 공유되는 것”에서 납니다. worktree는 작업 디렉터리와 HEAD, index만 따로 갖고, 나머지는 메인 저장소의 .git을 그대로 씁니다.
|
항목 |
worktree마다 따로 |
전체 공유 |
사고 포인트 |
|---|---|---|---|
|
작업 파일, HEAD, index |
O |
|
없음 (이게 worktree를 쓰는 이유) |
|
브랜치(refs/heads) |
|
O |
한 브랜치는 한 worktree에서만 체크아웃 가능 |
|
stash(refs/stash) |
|
O |
다른 세션의 stash가 내 목록에 보임 |
|
커밋 이력, 태그, 리모트 |
|
O |
한 쪽의 rebase·force push가 다른 쪽 기준점을 바꿈 |
|
git config, .git/hooks |
|
O |
한 쪽에서 바꾼 설정이 전부에 적용 |
|
.env, vendor/ 등 gitignore 파일 |
새 worktree엔 없음 |
|
서버가 안 뜨거나 엉뚱한 DB에 붙음 |
|
로컬 DB, 포트 |
|
O (git 밖) |
두 세션이 같은 DB에 migrate |
아래 두 사고는 표의 둘째·셋째 줄, CI4에서 추가로 챙길 것은 마지막 두 줄에서 나옵니다.
사고 1: 브랜치 충돌
브랜치 목록은 모든 worktree가 공유하지만, 한 브랜치는 동시에 한 worktree에서만 체크아웃할 수 있습니다. 이 제약 때문에 세 가지 형태로 부딪힙니다.
① AI가 브랜치를 옮기다 막힘. 세션이 스스로 git switch dev를 시도하면, dev가 이미 메인 체크아웃에 열려 있을 때 git이 거부합니다. 거기서 AI가 우회하려고 새 브랜치를 만들거나 detached HEAD로 작업을 이어가면, 나중에 커밋이 어느 브랜치에도 없는 상황이 됩니다.
fatal: 'dev' is already used by worktree at '/Users/me/claude-works/AICreo'
② 기준 브랜치가 예상과 다름. claude --worktree 이름은 기본적으로 리모트의 기본 브랜치(보통 main)에서 새 브랜치 worktree-이름을 만듭니다. feature → dev → main 흐름을 쓰는 프로젝트라면, 아직 main에 없는 dev의 변경을 모르는 채로 작업이 시작됩니다. 합칠 때가 되어서야 마이그레이션과 라우트가 한꺼번에 충돌합니다.
③ 같은 파일을 두 세션이 동시에 만짐. 파일은 분리돼 있으니 작업 중에는 문제가 없습니다. 대신 머지할 때 터집니다. CI4에서는 app/Config/Routes.php, app/Config/Filters.php, 같은 타임스탬프 근처의 마이그레이션 파일이 단골입니다.
대책은 단순합니다.
- 세션 하나 = worktree 하나 = 브랜치 하나. 세션 안에서는 브랜치를 바꾸지 않습니다.
- dev에서 따야 하면 git으로 직접 만들거나, 설정에서
worktree.baseRef를"head"로 둡니다(아래 규칙 섹션 참고). - Routes.php 같은 공용 파일을 건드리는 작업은 동시에 두 개 띄우지 않습니다. 띄워야 한다면 먼저 끝난 쪽을 dev에 넣고, 다른 쪽은 rebase 후 합칩니다.
사고 2: stash 공유
stash는 refs/stash 하나에 쌓이고, 이 ref는 모든 worktree가 같이 씁니다. 어느 worktree에서 git stash list를 해도 똑같은 목록이 나옵니다. 사람 혼자 쓸 때는 거의 문제가 안 되지만, AI 세션 여러 개가 동시에 쓰면 바로 사고가 됩니다.
전형적인 순서는 이렇습니다.
- 세션 A(feature-auth)가 테스트를 돌리려고 변경을 잠시
git stash합니다. →stash@{0} - 그 사이 세션 B(fix-invoice)도
git stash를 합니다. → B의 것이stash@{0}, A의 것은stash@{1}로 밀립니다. - 세션 A가 “아까 넣은 거 되돌리기”로
git stash pop을 실행합니다. - A의 브랜치에 B의 인보이스 수정이 들어가고, B의 stash는 목록에서 사라집니다. 충돌이라도 나면 AI가 “해결”까지 해버립니다.
AI는 stash 인덱스를 자기 것이라고 믿고, 다른 세션이 그 사이 목록을 바꿀 수 있다는 걸 모릅니다. 더구나 Claude Code의 worktree 격리는 메인 체크아웃을 건드리는 편집과 git 명령을 막는 것이라, worktree 안에서 실행되는 git stash는 그대로 통과합니다.
제 규칙은 “AI 세션에서는 stash를 아예 쓰지 않는다”입니다. 잠깐 치워야 할 때는 브랜치에 WIP 커밋을 남깁니다. 커밋은 브랜치에 붙어 있어서 다른 세션이 가져갈 수 없습니다.
# stash 대신
git add -A && git commit -m "wip: 테스트 전 임시 저장"
# 이어서 작업할 때 커밋만 풀기
git reset --soft HEAD~1
사람이 직접 stash를 써야 한다면 이름을 붙이고, pop 대신 확인한 ref로 apply한 뒤 지웁니다.
git stash push -m "feature-auth: 폼 검증 실험"
git stash list # 메시지로 내 것을 찾는다
git stash apply stash@{1} # 확인한 번호로
git stash drop stash@{1}
CI4 프로젝트에서 추가로 챙길 것
새 worktree는 깨끗한 체크아웃이라, git이 모르는 파일과 환경은 하나도 따라오지 않습니다. CI4에서는 다섯 가지가 걸립니다.
.env — 커밋하지 않으니 새 worktree에는 없습니다. 프로젝트 루트에 .worktreeinclude를 두면 Claude Code가 worktree를 만들 때 복사해 줍니다. gitignore된 파일만 복사 대상입니다.
# .worktreeinclude
.env
vendor/ — 역시 없습니다. worktree마다 composer install을 한 번 돌려야 PHPUnit과 PHPStan이 돕니다. CLAUDE.md에 “worktree에서 시작하면 먼저 composer install”을 적어 두면 매번 말할 필요가 없습니다.
데이터베이스 — 가장 조용히 터지는 곳입니다. .env를 그대로 복사하면 모든 worktree가 같은 DB를 봅니다. 세션 A가 만든 마이그레이션을 실행한 뒤, 세션 B가 php spark migrate:rollback을 하면 B에는 없는 마이그레이션 기록이 꼬입니다. worktree마다 DB 이름을 다르게 주고, 테스트는 tests 그룹을 따로 둡니다.
# worktree별 .env
database.default.database = aicreo_wt_auth
database.tests.database = aicreo_wt_auth_test
app.baseURL = 'http://localhost:8081/'
포트와 baseURL — 두 세션이 모두 php spark serve를 기본 포트로 띄우면 두 번째는 포트를 못 잡습니다. worktree마다 --port를 고정하고, 위처럼 baseURL도 맞춰 둡니다.
writable/ — 폴더 자체는 .gitkeep으로 따라오지만, 로컬 Apache로 띄울 경우 쓰기 권한을 다시 줍니다. 반대로 캐시·세션·로그가 worktree별로 나뉘는 건 장점입니다.
운영 규칙
위 내용을 실제로 지키는 규칙 여덟 개로 줄이면 이렇습니다. 말로만 정해 두면 AI가 잊으니, 막을 수 있는 건 설정으로 막고 나머지는 CLAUDE.md에 적습니다.
- 세션 하나 = worktree 하나 = 브랜치 하나. 세션 안에서 브랜치 전환 금지.
- 메인 체크아웃은 통합 전용. AI는 worktree에서만 일하고, dev로 합치는 일은 메인 체크아웃에서 한 번에 하나씩 합니다.
- AI는 stash 금지. 임시 저장은 WIP 커밋으로.
- 기준 브랜치를 명시. feature → dev → main 흐름이면
worktree.baseRef를"head"로 두거나,git worktree add로 dev에서 직접 딱니다. - 공용 파일은 한 세션만. Routes.php, Filters.php, 공통 마이그레이션을 건드리는 작업은 병렬로 돌리지 않습니다.
- DB·포트는 worktree별로. .env의 DB 이름, baseURL, serve 포트를 worktree마다 다르게.
- 합치기 전에 rebase. 먼저 끝난 브랜치가 dev에 들어가면, 나머지는 dev 위로 rebase하고 테스트를 다시 돌린 뒤 합칩니다.
- 끝난 worktree는 바로 정리. 하루 끝에
git worktree list와git stash list를 한 번씩 봅니다.
설정으로 막기
프로젝트 .claude/settings.json에 stash와 브랜치 전환을 deny로 겁니다. 파일 되돌리기는 git restore로 충분하니 git checkout도 같이 막아 둡니다.
{
"permissions": {
"deny": [
"Bash(git stash:*)",
"Bash(git switch:*)",
"Bash(git checkout:*)"
]
},
"worktree": {
"baseRef": "head"
}
}
.gitignore에는 Claude Code가 worktree를 만드는 경로를 추가합니다. 안 그러면 메인 체크아웃에서 untracked로 잡힙니다.
.claude/worktrees/
CLAUDE.md에 적기
## worktree 작업 규칙
- 현재 worktree의 브랜치에서만 작업한다. 브랜치를 바꾸거나 만들지 않는다.
- git stash를 쓰지 않는다. 임시 저장은 "wip:" 커밋으로 한다.
- 시작하면 composer install 후 .env의 DB 이름과 baseURL이 이 worktree 전용인지 확인한다.
- Routes.php, Filters.php를 고쳐야 하면 먼저 알리고 멈춘다.
일상 명령
# dev에서 새 작업 따기 (수동)
git worktree add ../AICreo-auth -b feature/auth dev
# 또는 Claude Code가 만들게
claude --worktree auth
# 하루 끝 점검
git worktree list
git stash list
# 정리
git worktree remove ../AICreo-auth
git worktree prune
맺음말
worktree는 파일을 나눠 주는 도구이지, 저장소를 나눠 주는 도구가 아닙니다. 브랜치와 stash, DB처럼 같이 쓰는 것을 먼저 알고 있으면, AI 세션 세두 개를 동시에 돌려도 사고 날 자리는 거의 없습니다.
결국 사람이 팀으로 일할 때의 상식과 같습니다. 한 사람이 한 브랜치를 맡고, 남의 서랍(stash)을 열지 않고, 합치는 건 한 자리에서 차례로 합니다. AI에게는 이걸 말이 아니라 설정과 CLAUDE.md로 알려 주면 됩니다.
참고: Claude Code 문서 – Run parallel sessions with worktrees, git-worktree 문서
댓글 0개