하네스를 실전에 적용하다: 병렬 에이전트 툴킷의 git 격리를 두 번의 적대적 리뷰로 다듬은 기록
'Agent = Model + Harness' 이론을 실제 Claude Code 툴킷에 적용한 케이스 스터디. 병렬 작업의 레이스 컨디션을 잡고, 두 번째 적대적 리뷰가 첫 리뷰에서 놓친 검증 우회를 어떻게 찾아냈는지 기록한다.
하네스 엔지니어링의 원리는 글로 읽으면 명료하다. 그러나 진짜 시험은 그 원리를 실제로 돌아가는 도구 위에 얹었을 때 시작된다. 이 글은 하네스/루프 엔지니어링 지형도에서 정리한 개념들을 우리 프로젝트(hiway-kit)에 실제로 적용하며 겪은 기록이다. 그리고 두 번의 적대적 리뷰가 우리가 “고쳤다고 믿은” 결함이 실제로는 남아 있었음을 어떻게 밝혀냈는지에 대한 이야기다.
핵심 요약
병렬 에이전트의 안전은 “격리 진입"이 아니라 “병합 복귀 프로토콜“과 공유 상태의 원자성에서 갈린다.
이론이 말한 “worktree 격리 + 병합"은 필요조건일 뿐이다. 격리만으로는 충돌이 사라지지 않고 병합 시점으로 이연될 뿐이다.
그리고 가장 값진 교훈은 “작성자 에이전트는 오염돼 있다“는 것이다. 자기 검증은 자기가 닫았다고 믿은 구멍을 놓친다. 세션을 분리한 독립 리뷰가 그것을 잡았다.
이 글은 2026년 7월 2일 작업(당시 v2.8.0)의 기록이다. 그 뒤 킷이 바뀐 부분은 본문에 “당시"로 표시하고, 끝의 “이후 바뀐 것"에 모았다.
1. 왜 ‘병렬’이 하네스의 급소인가#
이 툴킷은 여러 전문 에이전트를 병렬로 디스패치해 작업 속도를 끌어올린다. 각 에이전트가 파일을 동시에 수정하므로, 서로의 작업이 뒤섞이지 않도록 git worktree로 격리한 뒤 끝나면 다시 병합한다.
리서치가 확인한 업계의 수렴점도 정확히 이것이었다. 병렬 에이전트 도구들은 예외 없이 worktree(또는 컨테이너) 격리 + 태스크별 브랜치 + 리뷰 후 병합이라는 같은 설계에 도달했다. 하지만 같은 리서치가 남긴 경고가 있었다.
격리는 필요조건일 뿐, 충분조건이 아니다. 런타임 격리·충돌 예측·통합 리뷰가 함께 있어야 한다.
우리 툴킷을 이 잣대로 진단하자, 정확히 그 “충분조건"이 비어 있었다.
2. 진단: 격리 진입은 있는데, 병합 복귀가 없다#
첫째, 병합 프로토콜의 부재. 에이전트를 worktree로 “격리 진입"시키는 정책은 있었지만, 검증을 통과한 뒤 언제·어떻게 “복귀(병합)“하는지에 대한 규범이 없었다. 격리는 충돌을 없애는 게 아니라 병합 시점으로 미루는 것인데, 그 시점의 규칙이 없으면 미뤄둔 충돌이 한꺼번에 터진다.
둘째, 락 없는 공유 상태의 레이스 컨디션 3건. 병렬 세션이 같은 파일을 락 없이 읽고-고치고-쓰는 지점들이었다.
| 공유 상태 (당시) | 문제 | 결과 |
|---|---|---|
Stop 훅 재시도 카운터(/tmp 공유) | 세션 스코프 없이 repo 단위 공유 | 병렬 세션이 서로의 카운터를 덮어씀 |
| 피드백 원장(feedback ledger) | 병렬 리뷰 2개가 동시에 락 없이 기록 | lost-update: 한쪽 기록 소실 |
Work ID 채번(work.sh) | 번호 계산과 디렉토리 생성이 비원자적(TOCTOU) | 동시 생성 시 같은 ID 중복 발급 |
문제의 성격은 명확했다. “더 똑똑한 에이전트"로 풀 문제가 아니라 하네스의 구조적 결함, 즉 하네스 엔지니어링으로 봉인해야 할 실수였다.
3. 1차 구현과 1차 적대적 리뷰#
수정의 뼈대는 리서치가 정리한 두 축, feedforward와 feedback을 그대로 따랐다.
- feedforward(사전 조향): 병합 복귀 규범을 명문화한 규칙 파일(
rules/parallel-worktree.md)을 새로 만들었다. 당시에는 세션 시작 때 에이전트에게 주입됐다. 내용은 검증 통과 후 복귀, 파일 소유권 분리, 그리고 충돌이 나면 에이전트가 임의로 ours/theirs를 고르지 않고 git 워크플로 담당 에이전트로 에스컬레이션한다는 것이었다. - feedback(사후 관찰): 레이스 3건에 각각 세션 스코프 격리, 파일 락, 원자적 ID 선점을 적용하고 회귀 테스트로 봉인했다. 원장의 락은 가용성을 우선해, 5초 안에 잡지 못하면 경고를 남기고 락 없이 진행한다.
그리고 검증 원칙 하나, “작성자 에이전트는 오염돼 있다“를 따라, 코드를 작성한 세션이 아니라 분리된 세션의 적대적 리뷰어에게 검증을 맡겼다. 1차 리뷰는 곧바로 값을 했다. 심각도 High가 두 건 나왔다.
- 세션 스코프 적용 순서 버그: 카운터를 세션별로 격리하는 코드가 리셋 경로보다 늦게 실행돼, 정작 핵심 수정이 무력화되고 있었다. 고치려던 바로 그 결함을 새로 만든 셈이었다.
- 예측 가능한 임시 파일 경로: 공유 호스트에서 심볼릭 링크를 심어 임의 파일을 덮어쓸 수 있는 취약점(CWE-59)이었다.
열 건의 지적을 모두 수정하고 재리뷰에서 ACCEPT를 얻었다. 여기서 멈췄다면, 겉보기엔 완결된 작업이었다.
4. 2차 리뷰가 밝혀낸 것: ‘고쳤다고 믿은’ 우회#
멈추지 않고 한 번 더 돌렸다. 이번엔 여러 에이전트가 서로 다른 각도에서 동시에 파고드는 멀티에이전트 리뷰였다. 리뷰 에이전트들이 각자 결함을 찾고, 찾은 것마다 독립 검증자가 적대적으로 재확인했다. 검증을 통과한 지적 27건이 10개의 근본 원인으로 수렴했고, 그중 하나가 아팠다.
auto-dev 파이프라인은 검증을 마치면 “방금 검증했으니 Stop 훅은 이중 검증하지 말라"는 마커를 남기고, 그 안에 작업트리 상태의 지문(fingerprint)을 적는다. Stop 훅은 지문이 현재 상태와 같을 때만 검증을 건너뛴다. 1차 리뷰에서 이 지문이 새 파일을 놓친다는 지적을 받고 git status --porcelain을 추가해 “닫았다"고 판정했었다. 그런데 2차 리뷰가 물었다. porcelain은 추적되지 않는(untracked) 파일의 ‘경로’만 기록하지, ‘내용’은 반영하지 않는다. 그렇다면?
이미 untracked 상태인
.py파일의 내용을 검증 이후에 수정하면, 지문은 바이트 단위로 동일하게 유지된다. 마커는 여전히 유효하다고 판정되고, Stop 훅은 검증을 통째로 건너뛴다. 검증되지 않은 깨진 코드가 안전망을 조용히 통과한다.
1차에서 “닫았다"고 선언한 바로 그 우회가, 실제로는 열려 있었다. 자기 작업을 방금 검증한 세션의 눈에는 보이지 않던 구멍이었다. “검증자는 크지 않아도 되지만 반드시 달라야 한다“는 원칙이 실제로 작동한 사례다.
근본 재설계#
미봉책 대신 지문의 정의를 바꿨다. git status의 출력이 아니라, 훅이 실제로 검증하는 파일 집합의 ‘내용’을 직접 해시하도록 해서 지문의 대상과 검증의 대상을 구조적으로 일치시켰다. 그러자 untracked 내용 변경도 지문에 반영되어 우회가 차단됐고, 검증 대상이 아닌 파일(리뷰 결과 문서 등)의 변경은 지문에 영향을 주지 않아 마커가 불필요하게 무효화되지도 않았다. 이 지문 방식은 지금도 Stop 훅(hooks/stop-validator.py)에 그대로 있다.
2차 리뷰는 설계 수준의 모순도 하나 짚었다. 병합 규칙은 “메인 세션이 순차적으로 병합하라"고 적혀 있었는데, 정작 각 에이전트는 (당시) “검증이 끝나면 스스로 복귀하라"는 지시를 받고 있었다. 메인 세션에는 서브에이전트들의 복귀 타이밍을 직렬화할 수단이 없으니, 강제할 수 없는 규범이었다. 해법은 규칙을 실제로 통제 가능한 레버로 옮기는 것이었다. “순차 병합”(불가능)이 아니라 “순차 디스패치”(가능)로 바꾸고, 병렬 안전을 병합 시점이 아니라 디스패치 시점의 파일 분리로 확보하도록 다시 세웠다. 이 원칙은 지금 규칙 파일에 “순차 위임"이라는 이름으로 남아 있다.
5. 교훈: 하네스는 ‘실수를 시스템으로 봉인’하는 일이다#
“에이전트가 실수할 때마다, 다시는 그 실수를 못 하도록 시스템을 엔지니어링하라.” (Mitchell Hashimoto)
우리가 한 모든 수정은 결국 이 형태였다. 레이스가 가능하던 자리에 락과 원자적 연산을 넣고, 규범이 없던 자리에 규칙을 두고, 검증을 우회할 수 있던 자리에 검증 스코프와 지문을 일치시켰다. 그리고 그 과정을 봉인하는 회귀 테스트를 남겼다.
구현 → 적대적 리뷰(세션 분리)
├ 결함 있음 → 근본 원인 수정 → 회귀 테스트 봉인 → 적대적 리뷰로 ↺
└ 결함 없음 → 검증된 완료
가장 값진 한 가지는 이것이다. 한 번의 자기 검증은 자기가 만든 논리의 사각지대를 보지 못한다. 1차 리뷰가 ACCEPT를 준 뒤에도 멈추지 않고 다른 각도의 리뷰를 한 번 더 돌린 덕분에 배포 전에 진짜 우회를 잡았다. 검증은 “통과했다"는 주장이 아니라, 다른 눈으로 반복해서 반증을 시도한 뒤에도 살아남은 것이다.
자주 묻는 질문#
Q. worktree로 격리했는데도 왜 충돌이 생기나?
worktree 격리는 각 에이전트가 작업하는 동안 파일을 분리해줄 뿐, 하나의 브랜치로 합쳐지는 순간의 규칙은 제공하지 않는다. “언제·어떤 순서로·충돌 시 누구에게 넘겨 병합하는가"라는 복귀 프로토콜이 없으면 미뤄둔 충돌이 병합 때 한꺼번에 터진다.
Q. 리뷰를 한 번 받았는데 왜 또 받나?
코드를 작성한(혹은 방금 1차로 검증한) 주체는 자신이 세운 전제를 재확인할 뿐, 그 전제 자체를 의심하지 않는다. 실제로 1차 리뷰가 “닫았다"고 판정한 검증 우회가 2차 리뷰에서 여전히 열려 있었다. 검증자가 다른 각도·다른 세션이어야 사각지대가 드러난다.
결론: 모델이 아니라 루프를 고쳤다#
이번 작업에서 우리는 더 강한 모델을 기다리지 않았다. 같은 모델이 같은 실수를 반복하지 못하도록 루프를 설계하고, 검증하고, 정교화했을 뿐이다. 레이스는 락으로, 규범의 공백은 규칙으로, 검증 우회는 스코프 일치로 봉인했다. 그리고 그 전 과정을 두 번의 적대적 리뷰로 다른 눈에 통과시켰다.
모델은 평준화되고, 하네스는 차별화된다. 그리고 그 하네스의 품질은, 자기 자신조차 의심하는 검증 루프를 얼마나 정직하게 돌리느냐로 결정된다.
참고 자료#
- 하네스 & 루프 엔지니어링: 2026 중반 지형도: 이 글의 개념적 토대 (리서치 노트)
- Effective harnesses for long-running agents: Anthropic Engineering
- Harness engineering for coding agent users: Birgitta Böckeler (feedforward/feedback 프레임)
- My AI Adoption Journey: Mitchell Hashimoto
이후 바뀐 것#
2026-10-02 갱신.
- 병합 규칙은 이제 상시 주입되지 않는다.
rules/parallel-worktree.md는 v5.0.0부터 참조 등급이라, 세션에는 “병렬 위임·병합 전에 읽어라"라는 색인 한 줄만 들어간다. - 격리 수단을 특정하지 않는다. 규칙은 불변식(파일 소유권 분리, 임의 ours/theirs 금지, 격리 트리 안 공유 상태 갱신 금지)만 정하고, 네이티브 워크트리 격리는 여러 수단 중 하나로 다룬다.
- “스스로 복귀"는 “반환 후 통합"으로 바뀌었다. 작업자는 검증한 산출물을 반환하고, 지정된 통합 담당자만 병합한다. 작업자의 “그린” 보고는 게이트가 아니다. “순차 디스패치"는 “순차 위임"이라는 이름으로 남았다.
- 충돌 에스컬레이션은 해결이 아니라 보고다. git 워크플로 에이전트는 v2.17.0부터 충돌을 직접 풀지 않고, 되돌린 뒤 충돌 내용을 보고해 사용자가 고르게 한다.
- Work ID와
work.sh는 없어졌다. v4.0.0에서 Work 시스템 전체를 걷어냈다. 지금은docs/plans/<날짜>-<slug>/plan.md의 디렉토리 이름이 식별자라 채번 자체가 없다. 레이스 3번은 역사적 기록이다. - Stop 훅 상태 파일은
/tmp공유 경로를 떠났다. 본문 표의/tmp공유는 수정 전 상태다. 같은 작업의 2차 리뷰 수정에서 마커·재시도 카운터·원장 락을 사용자 전용 디렉토리($TMPDIR/claude-<uid>, 권한 0700)로 옮겼고, v3.34.2에서는 그 디렉토리가 심링크이거나 소유자·권한이 맞지 않으면 거부하고 공유 경로 대신 개인 임시 디렉토리로 폴백하도록 강화했다. - 피드백 원장 도구 위치가 바뀌었다. v5.2.0에서
hooks/에서plugins/common/tools/feedback_ledger.py로 옮겼다. 락 동작(5초 후 무락 진행)은 같다. - 2차 리뷰의 리뷰어 수는 뺐다. 원문의 리뷰 에이전트 수는 남은 기록으로 확인할 수 없어 삭제했다. 검증된 지적 27건은 변경 기록과 일치한다.