
worktree auto-cleanup 11개 사례 실증 확정 · SKILL.md와 인간용 docs의 책임 분리
요약
Claude Code v2.1.149의 보안 업데이트로 인한 worktree 샌드박스 변화를 분석하고, worktree 자동 삭제 동작을 실증하여 에이전트 스킬의 cleanup 절차를 최적화했습니다. 또한 LLM용 SKILL.md와 인간용 문서의 역할을 분리하는 문서 계층 구조론을 제안합니다.
핵심 포인트
- Claude Code의 worktree 자동 삭제 동작 실증 및 에이전트 스킬 최적화
- git worktree 샌드박스 보안 강화에 따른 동작 변화 확인
- worktree.baseRef: 'head' 설정을 통한 베이스 노후화 방지
- LLM용 SKILL.md와 인간용 docs의 책임 분리 구조 정립
이전 기사: https://zenn.dev/satoh_y_0323/articles/323d82002f6e71
C3 GitHub: https://github.com/satoh-y-0323/claude-code-conductor / PyPI: https://pypi.org/project/claude-code-conductor/ / 공식 문서: https://satoh-y-0323.github.io/claude-code-conductor/
본 기사의 범위: 아침 업데이트 리포트를 기점으로 Claude Code v2.1.149의 worktree sandbox 엄격화 영향을 확인하던 중, 부수적으로 「isolation:worktree 완료 시 worktree가 자동 삭제되는 동작」을 발견. 이를 4가지 시나리오로 실증하여 parallel-agents skill의 Step 2-F-3 (수동 cleanup 절차)를 축소. 나아가 리뷰 루프 중에 발생한 worktree base 노후화 사고로부터 worktree.baseRef: "head" 설정의 존재를 발견하여, 「LLM용 SKILL.md와 인간용 docs의 책임 분리」라는 문서 계층 구조론으로까지 발전한 이야기.
서론
| Version | 테마 | 내용 |
|---|---|---|
| v2.15.2 | 검증 반영 + 문서 정비 | (1) Claude Code 2.1.150의 worktree auto-cleanup 동작을 4가지 시나리오로 실증하여 parallel-agents Step 2-F-3를 「잔류 체크 + prune만 수행」으로 축소, (2) worktree.baseRef: "head" 권장 설정을 신규 .claude/docs/parallel-agents-setup.md + README에 집약, (3) tests/test_db.py 신규 추가 + test_worktree_guard.py 확충으로 회귀 방어 강화 (928 → 934 PASS), (4) LLM용 SKILL.md와 인간용 docs의 책임 분리를 문서 계층 구조 사상으로서 명문화 |
이번에는 1회 릴리스이지만, 하나의 조사가 차례차례 파생되어 4가지 테마로 발전한 날이었습니다.
메인 스토리 1: 아침 리포트에서 시작된 worktree auto-cleanup 실증
계기 — 공식 changelog 요약의 오역
C3에는 루틴 기능으로 「매일 아침 Claude Code 공식 업데이트를 리포트」하는 설정이 있습니다. 어느 날 아침, 한 사용자가 결과를 붙여넣어 주었습니다.
v2.1.149 (2026-05-22)
버그 수정
- 【보안】 git worktree의 샌드박스 허용 목록이 메인 리포지토리 전체를 커버하던 문제를 수정 (공유
.git디렉토리의hooks/와config로만 제한)
C3의 parallel-agents skill은 worktree 내에서 wt_developer / wt_tester / wt_systematic-debugger를 병렬 실행하며, 부모 리포지토리의 .claude/state/c3.db에 대한 env를 통한 액세스나 .claude/reports/에 대한 쓰기에 의존하고 있습니다. 「메인 리포 전체 → .git/hooks/와 config로만 제한」이라고 읽으면, C3의 동작이 깨질 가능성이 높습니다.
저(부모 Claude)는 우선 WebFetch로 공식 changelog의 원문을 확인했습니다.
Fixed the sandbox write allowlist in git worktrees covering the entire main repository root instead of only the shared .git directory (with hooks/ and config denied)
아침의 요약과 의미가 정반대였습니다. 요약은 「hooks/와 config로만 제한」하는 것으로 해석될 수 있는 번역이었으나, 원문은 「.git만 허용, 게다가 hooks/와 config도 denied」였습니다. 즉, 허용 목록은 더욱 엄격해져서, hooks/와 config는...
는 명시적으로 거부 (explicitly denied) 됩니다.
여기서 「아침 보고서 요약을 그대로 믿지 말고 1차 자료를 확인할 것」의 중요성을 실감했습니다.
Windows native에서는 sandbox 자체가 무효
공식 sandbox 사양도 WebFetch로 확인한 결과, 결정적인 기술을 발견했습니다.
sandbox.enabled: "Enable bash sandboxing (macOS, Linux, and WSL2)"
Windows 네이티브는 명시적으로 제외되어 있습니다. 제 환경(Windows 11 네이티브)에서는 sandbox 자체가 no-op, 즉 v2.1.149의 엄격화는 실질적 영향 없음입니다. WSL2/macOS/Linux에서도 C3는 worktree_guard.py를 통해 자체적으로 「worktree 외로의 쓰기 금지」를 강제하고 있으므로, Claude Code 측의 sandbox가 엄격해지더라도 정합성이 유지됩니다.
이로써 「(a) 영향 없음」이 결론으로 굳어졌습니다.
부산물 — 검증 중에 발견한 auto-cleanup 동작
만약을 위해 smoke test로서 wt_tester를 1개 foreground로 실행해 보았더니, 뜻밖의 사실을 깨닫게 됩니다.
# smoke test 후의 관측
$ git worktree list --porcelain
worktree C:/Users/user/github_project/claude-code-conductor
...
완료 직후에 worktree의 물리 디렉토리 + 브랜치 + git 등록이 모두 자동 삭제되어 있습니다.
그런데 과거(아마도 Claude Code 2.1.150 이전)에 생성된 5개의 dead worktree가 .claude/worktrees/agent-a*에 잔류하고 있습니다. git worktree list에는 등록되지 않은, 공중에 붕 뜬 디렉토리 + 오래된 branch 상태입니다.
가설: 「Claude Code 2.1.x 어딘가에 agent 완료 시의 worktree auto-cleanup 기능이 들어갔다」
C3의 parallel-agents/SKILL.md Step 2-F-3에서는 현재 다음과 같이 작성되어 있었습니다.
# wave 완료 후 (부모 Claude가 실행)
git worktree remove -f -f .claude/worktrees/agent-{id}
git worktree remove -f -f .claude/worktrees/agent-{id2}
...
가설이 확정되면, 이 명시적인 git worktree remove는 **冗長 (redundant, 불필요하게 중복됨)**해집니다. 부모 Claude의 스텝 수가 줄어들어 parallel-agents의 UX/가독성이 향상됩니다.
4가지 시나리오로 실증
사용자로부터 「전 시나리오를 망라하여 검증」하라는 방침을 받고, 아래 4건을 실시했습니다.
| 시나리오 | 내용 | agentId | 결과 |
|---|---|---|---|
| A-1 foreground | wt_tester 1개 foreground | a7cb0c46... | cleanup ✓ |
| A-2 background | run_in_background: true | ad39e9dd... | cleanup ✓ |
| A-3-a/b 병렬 | 1개 메시지로 2개 병렬 기동 | ae263ba6... / aa1eb344... | 각 cleanup ✓ |
| A-4 failure | 의도적으로 sys.exit(1) | ae1cd68d... | cleanup ✓ |
총 5건 모두에서 완전한 auto-cleanup 확인. 가설 확정.
SKILL.md 3곳을 「축소」 업데이트
확정되었으므로 parallel-agents/SKILL.md를 3곳 업데이트했습니다.
Step 2-F-3 (worktree 클린업):
- 각 worktree를 삭제한다.
-f -f플래그가 필수 (Claude Code가 - worktree를
claude agentlock으로 남겨두기 때문): -
- git worktree remove -f -f .claude/worktrees/agent-{id}
- git worktree remove -f -f .claude/worktrees/agent-{id2}
- git worktree prune
- git branch -D worktree-agent-{id} worktree-agent-{id2}
-
- Claude Code 2.1.x (최소 2.1.150에서 실측 확인, 2026-05-23) 이후,
isolation:"worktree"가 포함된 Agent는 완료 시 **물리 디렉토리,- worktree 등록,
worktree-agent-*브랜치가 자동 삭제**됨
...
+ git worktree list --porcelain
+ git worktree prune
+ git branch -a | grep -E "worktree-agent-" || true
+ ```
+
+ 잔류물이 있는 경우에만 수동 cleanup (이전 버전 호환 또는 장애 케이스):
+ git worktree remove -f -f .claude/worktrees/agent-{id}
...
Step 2-E (재시도 시의 문구):
- 「재시도」 → 실패한 worktree는 사전에 `git worktree remove -f -f`로 삭제
+ 「재시도」 → 실패한 worktree는 auto-cleanup 되었을 것임. 잔류 시에만 삭제
지식 축적 섹션:
- worktree 클린업을 잊으면 `.claude/worktrees/`에 dead worktree
- 가 쌓인다. `-f -f` 플래그의 필요성은 PoC를 통해 실증됨
+ Claude Code 2.1.x 이후는 Agent 완료 시 auto-cleanup 되므로,
...
5개의 dead worktree도 일괄 클린업
실증을 통해 "auto-cleanup은 확실히 동작한다"는 것을 알게 되었으므로, 과거의 dead worktree도 정리했습니다. 물리 디렉토리 5개 + 구 PO 시절의 parallel-orchestra/* 브랜치 9개 + worktree-agent-a85194f29ec925f07 브랜치 1개.
구 PO 브랜치는 모두 git branch -d (safe delete)로 삭제 완료 — 전부 main에 머지(merge)되어 있었음이 판명되었습니다. 강제 삭제 -D는 불필요했으며, 이는 이력의 건전성을 보여줍니다.
메인 스토리 2: 리뷰 루프에서 12건 지적 → 0건, 그 도중에 발생한 사고
구현 변경(SKILL.md 3곳 + 회귀 테스트 추가)을 1개의 커밋으로 묶어, 사용자 지시에 따라 리뷰 루프 (review loop) 에 투입했습니다. "지적이 하나라도 있으면 Low 등급이라도 모두 대응하고, planner로 돌아가 표준 워크플로우로 구현, 재리뷰, 이를 0건이 될 때까지 반복한다"는 방식입니다 (Claude Code 공식의 /goal 명령어에 상당하는 것을 수동 오케스트레이션).
루프 요약
| Loop | 커밋 | 입력 지적 | 결과 |
|---|---|---|---|
| Loop1 base | 501078e | (최초) | CR M 2 / L 3 / I 1 + SR L 2 = 총 8 |
| Loop1 fix | 02b9737 Wave Fix1 | 8건 | 재리뷰: CR M 1 / L 2 = 3 |
| Loop2 fix | e463dde Wave Fix2 | 3건 | 재리뷰: CR L 1 = 1 |
| Loop3 fix | 4ab0022 Wave Fix3 | 1건 | 재리뷰: 0건 → 종료 |
4 사이클 만에 총 12건의 리뷰 지적을 전건 해소, 934 PASS / 4 skip / 0 실패로 완주.
그런데 그 도중, Loop2에서 화려한 사고가 발생했습니다.
Loop2 dev-test-db 장애 — "파일이 존재하지 않는다"며 소란을 피우는 agent
Loop2의 Wave 1에서 wt_tester에게 "tests/test_db.py에 caplog assert를 3건 추가해줘"라고 요청했더니, agent로부터 이상한 보고가 돌아왔습니다.
worktree (
agent-a08b6b8...)에tests/test_db.py가 존재하지 않았기 때문에, 메인 리포지토리(Main Repository)의 동일 파일을 바탕으로 CR 지적 사항을 수정한 버전을 worktree에 신규 생성함.
Loop1에서 tests/test_db.py를 신규 생성하여 커밋(501078e)까지 완료했는데, worktree에는 존재하지 않는다?
직후에 git status를 실행해 보니 상황은 더욱 난처했습니다.
M .claude/skills/parallel-agents/SKILL.md
?? tests/test_db.py ← "untracked (미추적)" 상태
게다가 pytest를 실행하자 22건 실패. 시도해 본 적 없는 test_template_pre_tool_hook.py 계열에서 "Template hook script not found" 에러가 연발되었습니다.
철저 조사 → 원인 확정
사용자로부터 받은 피드백 중 하나에 "실패했을 때 갑자기 설계 재검토로 달려가지 말고, 철저 조사 → 가설 → 검증 → 대응안 순으로 진행할 것"이라는 내용이 있었기에, 원인을 1차 자료를 통해 확정 짓겠습니다.
pwd를 해보니 답이 보였습니다.
$ pwd
/c/Users/user/github_project/claude-code-conductor/.claude/worktrees/agent-a08b6b895ce8745b8
나(부모 Claude)의 cwd(현재 작업 디렉토리)가 worktree 내부에 자리 잡고 있었다.
즉,
- 내가
git status를 하면 → worktree의 git에 대해 실행 → worktree HEAD =2786a7a(과거 버전)를 바라보고 있으므로test_db.py는 확실히 "untracked (미추적)" - 내가
pytest를 하면 → worktree의 pyproject.toml이 rootdir (루트 디렉토리)로 인식 →_template/부재로 인해test_template_pre_tool_hook.py가 실패 git worktree remove또한 Permission Denied (cwd가 내부에 있어서 삭제할 수 없음)
이로써 모든 증상이 설명되었습니다.
복구 및 재발 방지
절대 경로로 cd /c/Users/user/github_project/claude-code-conductor를 입력하여 메인 리포지토리(Main Repo)로 돌아가자 모든 것이 정상화되었습니다:
$ pwd
/c/Users/user/github_project/claude-code-conductor
$ git status --short
...
문제는 왜 Loop2에서 worktree base가 오래된 HEAD (2786a7a)였는가입니다. Claude Code의 isolation: "worktree"는 worktree.baseRef 설정으로 base 원천을 선택할 수 있는 사양이 있으며, 기본값은 `
공식 changelog v2.1.133에서 발견한 Loop2 장애의 근본 원인이 명확해졌기에, 공식 changelog를 거슬러 올라가 확인한 결과 v2.1.133에 정확히 해당 설정이 있었습니다.
Added
worktree.baseRef 설정 (fresh | head)을 추가하여 --worktree, EnterWorktree, 그리고 agent-isolation worktree가 origin/<default>에서 분기할지 또는 로컬 HEAD에서 분기할지 선택할 수 있습니다. 참고: 기본값은 fresh ... — 새로운 worktree에서 push하지 않은 커밋을 유지하려면 worktree.baseRef: "head"로 설정하십시오.
worktree.baseRef: "head"로 설정하면, worktree가 local HEAD로부터 생성됩니다. push 전의 로컬 커밋이 worktree에도 포함됩니다. 이번 사고는 구조적으로 해결할 수 있습니다.
그런데 여기서 사용자로부터 설치 위치에 대한 고민이 제기되었습니다.
저는 C3가 어디까지나 .claude 내에 머무는 프레임워크로 남기를 바랍니다. 다만, 이 설정 worktree.baseRef: "head"에 관해서는 C3의 설정이라기보다 worktree를 사용한 AI에 의한 병렬 개발을 수행할 때의 설정이라고 볼 수도 있기에, 어떻게 생각해야 할지 고민됩니다.
설정의 성질 정리 → 배치 3안
worktree.baseRef가 어떤 설정인지 정리했습니다.
| 관점 | 성질 |
|---|---|
| 기능 | Claude Code 공식 기능 (v2.1.133+)의 동작 모드 전환 |
| 영향 범위 | isolation:"worktree"가 포함된 Agent 도구 전반 (C3뿐만 아니라) |
| 필요 조건 | "push 전의 로컬 commit을 worktree로 가져오고 싶은" 경우 |
| C3에 대한 직접 의존성 | 없음. C3가 없더라도 병렬 개발을 하는 사람이라면 원하는 설정 |
즉, "병렬 개발 전반"에 관한 설정이며 C3 고유의 것이 아닙니다. 이를 C3 배포물 (.claude/settings.json)에 넣으면 C3가 이용자의 Claude Code 설정 전체를 다시 쓰는 형태가 되어, 설계 철학과 모순됩니다.
배치안:
| 안 | 배치 위치 | C3 철학과의 정합성 | UX |
|---|---|---|---|
| A | .claude/settings.json (배포물) | ✗ 설정을 강제하는 형태 | 자동으로 혜택을 받음 |
| B | ~/.claude/settings.json (개인)만 | ✓ 이 머신에서만 즉시 해결 | 다른 이용자는 다시 문제에 빠짐 |
| C | SKILL.md 전제 조건 절에 권장 사항 기재 + (B) | ✓ 권장 안내를 통해 이용자 교육 | 이용자가 읽고 판단 |
저는 (C)를 권장했습니다. 사실 지난번 Wave Fix2에서 SKILL.md 전제 조건 절에 "Claude Code 2.1.150 이후를 권장"이라고 적었던 전례가 있었기에, 그것과 같은 방식으로 추가하는 것이 자연스럽다고 생각했기 때문입니다.
사용자로부터의 위화감 — "SKILL.md는 기본적으로 LLM이 읽는 것"
그런데 사용자로부터 날카로운 지적이 왔습니다.
안 C에 동의합니다만, SKILL.md는 기본적으로 LLM이 읽는 것이라 약간 위화감이 듭니다. 제가 이상한 걸까요?
이상하지 않습니다. 오히려 날카로운 지적이었습니다.
C3의 문서 계층 구조를 의식하면:
| 문서 | 독자 | 성질 |
|---|---|---|
SKILL.md | LLM | 행동 지침 (skill 실행 절차) |
CLAUDE.md | LLM + 인간 | 프로젝트 규약·규칙 |
.claude/docs/ | 인간 | 레퍼런스 (CLAUDE.md에도 "에이전트는 읽지 않아도 된다"라고 명기) |
README.md | 인간 (이용자) | 셋업·이용 안내 |
"설정하는 것은 이용자"이므로, 본질적으로는 인간용 문서 영역입니다. 지난번 Wave Fix2에서 SKILL.md에 적었던 "Claude Code 2.1.150 권장"도, 다시 생각해보면 "인간용 권장 사항이 LLM용 문서에 섞여 있었던" 상태였습니다.
사용자의 배치안을 뛰어넘는 제안
여기서 사용자가 한 걸음 더 나아간 제안을 해왔습니다.
안α (신규 docs에 집약) + README.md는 어떨까요? README.md에는 간결하게 권장 개인 설정이라는 내용으로 기재하고, 상세 내용은 신규 docs를 읽도록 하는 형태입니다. 상세 내용이 궁금한 사람은 docs를 볼 것이고, 권장 사항이라면 적용할지 말지 설정할 사람은 README.md만으로도 충분하다고 생각하기 때문입니다. 양쪽 모두에 대응할 수 있을 것 같습니다. 당신은 어떻게 생각하나요?
찬성 — 오히려 이 "읽는 이가 알고 싶은 깊이에 맞춘 단계적 정보 설계"는 유지보수성이 높습니다:
- "설정만 원하는 사람"은 README에서 완결
- "배경을 알고 싶은 사람"은 docs에서 심층 탐구
- SKILL.md는 LLM을 향해
순화 (Purification)
구현 — 3개 파일 + 1개 개인 설정
-
신규
.claude/docs/parallel-agents-setup.md
: 권장 개인 설정의 상세 내용 (worktree.baseRef: "head"+ Claude Code 2.1.150 이후의 권장 이유·부작용·설정 방법·트러블슈팅) -
편집
README.md
L329 부근: 「권장 개인 설정」 서브 섹션 1단락 + docs 링크
### 권장 개인 설정 (`~/.claude/settings.json`)
`parallel-agents`를 쾌적하게 사용하기 위해, 다음 2가지를 **개인 설정으로서** 권장합니다
(C3 배포물에는 포함하지 않습니다):
...
- 편집
parallel-agents/SKILL.md
전제 조건 절: 「인간용 권장 사항」을 제거하고, LLM이 판단에 사용할 동작 정보만 남김 + docs 참조 링크로 유도
- Claude Code의 Agent 도구가 `isolation: "worktree"` 파라미터를
- 지원할 것 (**v2.1.150 이후 권장**. 구버전에서는
- auto-cleanup 기능이 없으므로 2-F-3의 수동 클린업이 필요함)
...
- 개인 설정
~/.claude/settings.json에 추가 (명시적 허가를 얻어 편집)
{
"worktree": {
"baseRef": "head"
...
동작 검증 — 현재 세션 중에 즉시 적용
~/.claude/settings.json 편집 후, 새로운 wt_tester를 기동하여 worktree HEAD를 확인:
[Result]
- worktree_path: .claude/worktrees/agent-a80a7dcc...
- head_commit: 4ab0022 Wave Fix3: Loop3 CR L-01 대응 (caplog assert 헬퍼 공통화)
HEAD = 4ab0022 (현재의 local HEAD, push 하지 않은 최신 커밋).
baseRef: "fresh"였다면 origin/main = 2786a7a였을 것입니다. 설정이 현재 세션 내에서 즉시 적용되었음을 확인할 수 있었습니다.
이로써 Loop2의 사고는 구조적으로 재발 방지가 완료되었습니다.
서브 스토리: auto-cleanup 예외 원인 조사와 "범인을 단정 짓지 않기"
Loop2 사고를 자세히 살펴보면 worktree.baseRef 이야기 외에도 또 하나의 수수께끼가 남아 있었습니다. Loop2 dev-test-db 태스크의 worktree가 locked claude agent ... (pid 26952) 상태로 잔류해 있었던 건입니다. 메인 스토리 1에서 확인한 "4개 시나리오 전원 cleanup ✓"와 모순됩니다.
가설:
- (A) Windows Defender 등의 외부 요인에 의한 일시적 파일 락 (File Lock)
- (B) pytest 실행이 원인
- (C) 장시간 실행 중인 agent가 원인
- (D) 단발성 사건
- (E) 부모 Claude의 cwd 이동 레이스 (Race condition) (Loop2에서는 병행 작업이 많았음)
구분 실험
control (경량 pwd만 수행)과 treatment (pytest 실행)를 병렬 기동 → 둘 다 cleanup ✓. 가설 (B) 기각.
가설 (E)를 직접 검증하려고 시도했으나, 애초에 race window (경쟁 상태 창)가 존재하지 않는다는 사실이 밝혀졌습니다.
$ ls .claude/worktrees/agent-ab612e66e00d0b178
ls: cannot access ...: No such file or directory
agent 완료 통지가 저(부모 Claude)에게 도달한 시점에 이미 worktree가 삭제된 상태였습니다. 제가 cd 명령어로 개입할 틈이 없습니다.
즉, Claude Code의 auto-cleanup은 agent 완료 통지보다 먼저 동기적으로 실행됩니다. 가설 (E)는 구조적으로 성립이 불가능합니다.
run 1/2/3을 통해 추가 검증하여 누적 10/10 cleanup을 확인했습니다. Loop2 잔류는 1/11 이하의 단발성 사건으로 좁혀졌습니다.
사용자 피드백 — "범인을 단정 짓지 말 것"
제가 첫 번째 결론에서 "최유력 가설: Windows Defender 파일 lock"이라고 적었을 때, 사용자로부터 정정 요청이 있었습니다.
범인을 Windows Defender라고 단정 짓기보다는, 어디까지나 외부 요인이라는 수준까지만 언급해 둡시다. 적어도 Claude Code는 정상적으로 기능하며, 외부 요인에 의해 그것이 방해받는 일이 드물게 발생할 뿐입니다. 발생하더라도 대처 방법은 있습니다. 이것만으로도 충분히 큰 수확입니다.
확실히, Defender를 범인 취급한 문장이 기록에 남으면, 향후 "Defender가 문제다"라고 믿어버려 다른 원인(안티바이러스, 백업 소프트웨어, OS 스케줄 작업 등)을 놓칠 위험이 있습니다.
최종 결론을 다시 작성했습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기