Claude Code를 헤드리스로 구동하는 개발 하네스 설계: 공정, 권한, 중단 처리 방식 분리
요약
본 글은 AI가 설계, 구현, 테스트까지 진행하는 자동화된 개발 시스템(하네스)의 핵심 설계 원칙을 다룹니다. Claude Code를 헤드리스 모드로 구동하고, 공정별로 권한과 도구를 분리하며, 실패 시 같은 세션을 재개하여 수정하도록 하는 것이 주요 내용입니다.
핵심 포인트
- AI 개발 시스템은 공정(Process) 단위로 나누어 관리합니다.
- 각 공정마다 필요한 최소한의 권한과 도구만 부여합니다.
- 작업 격리를 위해 전용 worktree와 브랜치를 사용합니다.
- 테스트 실패 시 세션 재개 기능을 통해 반복적인 수정을 유도합니다.
저는 개인적으로 소프트웨어 개발을 하는 엔지니어입니다.
백로그에 과제를 쌓아두면, AI가 설계, 구현, 테스트, 다른 모델의 리뷰, 머지까지 진행하는 개발 시스템을 직접 만들었고, 2026년 7월부터 8월을 중심으로 운영해 왔습니다.
지난 글에서는 이 시스템의 전체적인 개요와 운영 중 겪었던 함정들을 작성했습니다.
이번에는 그 연장선으로 설계의 핵심 포인트를 항목별로 작성하겠습니다.
각각에 '왜 그렇게 했는지'를 첨부합니다.
소스 코드는 공개하지 않기 때문에, 생각하는 방식은 몇 줄의 의사 코드와 설정 설명으로 보여드리겠습니다.
전제: 공정마다 Claude Code를 1회 구동한다
이 시스템은 Claude Code를 헤드리스 모드(-p)로 자식 프로세스로 구동하고, 공정별로 결과를 받아 연결합니다.
출력은 --output-format json으로 받고, 세션 ID, 비용, 종료 이유를 기계적으로 읽어옵니다.
사소한 것이지만, 프롬프트는 인수가 아니라 표준 입력으로 전달합니다.
--allowedTools처럼 값을 여러 개 받는 옵션이 마지막에 붙인 프롬프트까지 삼켜버리는 경우가 있었기 때문입니다.
모든 구동에는 --append-system-prompt로 공통의 안전 규칙을 선행 배치합니다.
'자신이 시작하지 않은 프로세스를 멈추지 않는다', '사용 중인 포트를 빼앗지 않고, 다른 빈 포트를 사용한다'와 같은 내용입니다.
손에서 개발용 서버를 운영하는 경우가 있어, 권한을 넓게 준 자동 실행이 그것을 멈추지 않도록 하기 위함입니다.
1. 작업은 태스크별 worktree에 격리한다
태스크를 시작할 때마다 개발 브랜치에서 전용 브랜치와 git의 worktree를 만듭니다.
AI는 그 안에서만 작업하고, 끝나면 worktree를 정리합니다.
이유는 세 가지가 있습니다.
첫째, 저의 손 작업 트리나 커밋되지 않은 변경 사항에 일절 건드리지 않기 위함입니다.
둘째, 태스크를 병렬로 돌려도 서로의 파일이 충돌하지 않기 위함입니다.
셋째, 실패했을 때 브랜치를 남겨서 사람에게 인계할 수 있기 위함입니다.
병렬로 돌리는 경우에도, 머지나 브랜치 삭제 같은 공유 git 작업만은 프로젝트별 큐에서 직렬로 처리합니다.
테스트나 에이전트 실행처럼 시간이 오래 걸리는 처리는 락(lock) 바깥에서 병렬로 진행됩니다.
더불어, AI 자체에게는 push, 머지, 브랜치 전환을 시키지 않기로 규칙을 정했습니다.
이것들은 하네스의 공정입니다.
원격 저장소가 먼저 진행되는 상태에서 AI가 push하여 실패하는 사고가 몇 번 있었기 때문입니다.
2. 공정을 나누고, 공정마다 전달하는 도구를 바꾼다
한 번의 구동으로 모든 것을 맡기지 않고, 공정을 나눕니다.
공정마다 전달하는 권한을 다르게 합니다.
- 설계는
--permission-mode plan
으로 구동하여 읽기 전용으로 구현 방침만 쓰게 합니다. 그 방침을 구현 담당자에게의 인수인계로 프롬프트에 넣습니다. - 구현만 쓰기 권한을 갖습니다. - 리뷰는
--tools
로 Read, Grep, Glob 세 가지로 제한합니다.
설계를 나눈 것은, 수정할 수 없는 상태에서 먼저 방침을 세우게 하고, 구현 담당자에게는 그 방침에 따라 쓰게 하려고 했기 때문입니다.
다만, 설계는 필수 관문으로 삼지 않았습니다.
설계 출력이 비어 있어도, 태스크는 실패하지 않고 인수인계 없이 구현을 진행합니다.
보조 공정에서 멈추면, 본체 작업까지 멈춰버리기 때문입니다.
리뷰를 읽기 전용으로 한 것은, 판별하는 쪽이 스스로 고쳐버리면, 무엇을 지적했는지 기록이 사라진다고 생각했기 때문입니다.
3. 테스트가 떨어지면, 같은 세션을 재개하여 수정하게 한다
구현이 끝나면, 하네스가 설정된 테스트와 체크 명령을 실행합니다.
떨어진 경우, 구현의 세션 ID를 --resume에 전달하여 같은 대화를 재개하고, 테스트 출력을 주어 고치게 합니다.
for 시도 in 0..수정 상한:
테스트를 실행
성공하면 끝
...
새로운 세션에서 고치지 않는 이유는, 구현 담당자가 이미 변경의 의도와 주변 코드를 기억하고 있기 때문입니다.
다른 세션에서 고치게 하면, 같은 코드를 다시 읽어보는 것부터 시작하여, 원래의 의도를 모른 채 고치게 됩니다.
수정 상한은 기본으로 3회입니다.
리뷰 지적에 대한 대응과, 머지 직전 충돌 해소에도, 같은 재개 메커니즘을 사용합니다.
4. 리뷰는 별 모델, 읽기 전용, JSON 스키마로
리뷰는 구현과는 별도의 세션에서, 다른 모델을 사용해서 진행합니다.
기본적으로는 구현보다 상위의 모델을 리뷰에 투입하고 있습니다.
같은 모델에게 자신의 결과물을 보여주면, 관대하게 승인하기 쉽다고 생각했기 때문입니다.
판정은 --json-schema로 형식을 정해서 반환하도록 합니다.
- 판정은 '승인' 아니면 '수정 요청'의 두 가지 선택지입니다.
- 지적 사항마다, 대상 파일, 내용, 무게(중요도), 병합을 멈춰야 하는지 여부를 포함하도록 합니다.
프롬프트에서는 승인이 될 경우 그대로 개발 브랜치에 병합된다는 것을 전달하고 있습니다.
반면, 스타일 선호나 가벼운 명명 규칙은 병합을 멈출 지적 사항으로 만들지 않도록 지시합니다.
되돌려 받는 것은 병합을 멈춰야 하는 지적 사항만이며, 그것을 구현 세션에 넘겨서 수정하게 합니다.
라운드 상한은 기본적으로 2회이며, 초과하면 브랜치를 남기고 사람에게 돌려줍니다.
형식을 정해서 반환하도록 한 것은, 판정을 문장에서 읽으면 하네스의 분기가 불안정해질 것이라고 생각했기 때문입니다.
그럼에도 불구하고, 빈 응답이나 내용 없는 되돌림이 돌아오는 경우가 있습니다.
이것들은 정상적인 판정으로 취급하지 않고, 재시도합니다.
빈 응답이 계속되는 것은 모델 측의 이상일 수 있으므로, 재시도의 마지막 1회는 구현과 동일한 모델로 전환합니다.
기록상 완료된 239건 중 203건은 첫 번째 리뷰에서 승인되었습니다.
2차에서 승인된 것이 30건, 3차에서 승인된 것이 4건입니다.
남은 2건은 리뷰를 거치지 않고 완료했습니다.
5. 병합은 PR을 통해 진행하고, 검사 결과도 PR에 남기기
병합은 태스크별로 GitHub의 PR(Pull Request)을 만든 후 squash 방식으로 진행합니다.
PR 본문에는 과제 내용, 수용 조건, AI 리뷰 요약본을 넣습니다.
나중에 '이 변경은 무엇 때문에, 누가 어떻게 확인했는지'를 PR 단위로 추적할 수 있도록 하고 싶었기 때문입니다.
origin이 없는 프로젝트에서는 로컬에서 직접 병합하는 것으로 전환합니다.
병합 직전에는 최신 개발 브랜치를 가져옵니다.
병렬 태스크가 먼저 병합하면 충돌이 발생하기 때문에 그렇습니다.
충돌이 발생하면 구현 세션을 재개하여 해결시키고, 테스트를 다시 통과시킨 후에 병합합니다.
태스크 로그를 계산해 보니, 이 충돌 해결을 거친 태스크는 5건 있었습니다.
CI(Continuous Integration)에 GitHub Actions는 사용하고 있지 않습니다.
비용이 발생하는 시스템은 피하고 싶었기 때문입니다.
대신 테스트와 리뷰 결과를 GitHub의 커밋 스테이터스 API로 PR에 기록합니다.
PR 화면에서 어떤 검사를 통과했는지 알 수 있습니다.
6. 병합 후에도 다시 한번 검증하고, 고장났으면 복구 태스크를 쌓기
병합이 끝나면, 개발 브랜치를 일회용 worktree로 가져와 테스트와 체크를 다시 실행합니다.
하나하나의 태스크는 통과했더라도, 직전 병합과의 조합으로 인해 깨질 수 있기 때문입니다.
실패한 경우에는, 실패 출력을 첨부한 복구 태스크를 우선순위를 가장 높게 하여 백로그에 쌓습니다.
이미 미착수된 복구 태스크가 있다면, 중복해서 쌓지 않습니다.
복구 태스크 구현에서 차이점이 없는 상태로 테스트가 통과된 경우에는, 이미 고쳐진 것으로 간주하고 완료 처리합니다.
이것을 실패로 하면, 자동 재시도가 공회전을 반복하기 때문입니다.
기록상 이 경로로 복구 태스크가 쌓인 것은 3회였습니다.
그중 1회는 테스트 커맨드가 끝나지 않아 시간 초과된 것에 의한 오탐지로 판단하여 수동으로 취소했습니다.
남은 2건은 모두 완료되었습니다.
7. 개선 제안 담당자에게 다음 과제를 생각하게 하기
백로그가 비게 되면, 개선 제안 담당자가 다음 과제를 생각합니다.
개발 브랜치의 일회용 worktree 위에서, 읽기 전용 도구만 사용해서 코드를 조사하고, 제안을 JSON으로 반환합니다.
제안에는 영향의 크기와 공수를 5단계로 자체 채점하게 하며, 영향이 크고 공수가 작은 것일수록 높은 점수를 받도록 합니다.
제안의 품질을 유지하기 위해 다음 장치를 넣었습니다.
- 다른 호출로 제안을 비평하게 하고, 피상적이라고 판정된 것만 1회 다시 작성하게 합니다.
다시 작성에 실패한 것은 버립니다. - 기존 태스크와 제목이 비슷한 제안은 쌓지 않습니다.
- 사람이 한 번 거절한 제안과, 그것과 유사한 제안은 야간 자동 실행에서도 다시 쌓지 않습니다.
- 과거의 실패로부터 기록된 교훈과, 최근 완료 및 실패 결과를 프롬프트에 넣습니다.
- 사양이 있는 경우, 사양과 구현을 대조하여 미구현 필수 기능부터 제안하게 합니다.
양보다 질을 선택한 이유는, 얕은 제안들이 쌓이면 그것을 구현하는 비용이나 리뷰의 수고가 헛되다고 생각했기 때문입니다.
8. 예산과 시간의 상한선, 그리고 중단 분류
모든 실행에는 시간의 상한선과 1회당 예산의 상한선을 설정합니다.
시간의 상한선은 AI의 1회 실행이 기본적으로 30분, 테스트 명령어는 기본적으로 10분입니다.
상한선을 두면 당연히 중단(打ち切り)이 발생합니다.
그래서 실패를 일률적으로 다루지 않고, 이유별로 분류하여 처리 방식을 달리하고 있습니다.
예산 초과 처리는 지난 글에서 작성했습니다.
시간 초과도 마찬가지로, 중간 경과를 커밋(commit)하고 다음 시도를 이어서 시작합니다.
여기서는 나머지 세 가지를 보완합니다.
사용량 상한에 도달했을 때는 태스크를 실패로 처리하지 않고 대기 상태로 되돌립니다.
지금은 무엇을 실행해도 실패하는 상태이므로, 실패로 처리하면 후속 태스크까지 연쇄적으로 실패하여 큐가 전멸하기 때문입니다.
상한선이 돌아오는 시간을 읽어 들여, 그때까지는 다음 실행도 막습니다.
GitHub의 CLI를 사용할 수 없거나, 로컬 리포지토리에 미커밋된 변경 사항이 남아 있는 것과 같은 환경 문제는 태스크의 시도 횟수에 계산하지 않습니다.
태스크를 대기 상태로 되돌려 루프 자체를 멈추고 사람에게 수정하도록 합니다.
방치하면 후속 태스크가 모두 같은 곳에서 실패하고, 비용만 쌓이기 때문입니다.
같은 이유로, 병합(merge) 단계에서 확실하게 실패하는 환경 문제는 구현을 시작하기 전에 확인합니다.
중단 상태에서 돌아올 때 처음부터 다시 하지 않도록, 구현 후, 리뷰 승인 후, 병합 직전 세 곳에 재개점(再開点)을 기록하고 있습니다.
브랜치가 남아 있다면, 재개점의 다음 부분부터 처리합니다.
또한, 실행할 때마다 PR을 가진 미완료 태스크를 GitHub 상의 실제 상태와 대조합니다.
이미 병합된 것이라면 완료로 바꾸고, 같은 태스크를 다시 구현하지 않도록 합니다.
완료된 태스크 중 1회 실행으로 완료된 것은 217건, 재실행을 거쳐 완료된 것은 22건이었습니다.
9. 상태는 파일에 두고, 대시보드는 그것을 읽는다
백로그(Backlog), 교훈(教訓), 비용 기록, 태스크별 로그는 모두 프로젝트 내의 파일에 두고 있습니다.
형식은 JSON, JSON Lines, Markdown이며, 데이터베이스는 사용하지 않습니다.
명령줄과 대시보드는 같은 파일을 읽고 씁니다.
대시보드는 칸반(Kanban), 각 태스크의 공정 이력, 태스크 로그, 개선 제안의 승인 및 거절을 하나의 화면에 모아 놓은 것입니다.
실행 프로세스와 분리되어 있기 때문에, 대시보드가 다운되어도 실행은 멈추지 않습니다.
상태가 파일에 있으므로, 실행 프로세스가 다운되어도 다음 실행에서 진행 중이던 태스크를 다시 가져올 수 있습니다.
백로그 작성은 임시 파일에 작성한 후 덮어쓰는 방식으로, 1세대 전의 사본을 남깁니다.
작성 직전에는 디스크에서 다시 읽습니다.
실행 중에 화면에서 태스크를 추가했을 때, 실행 측의 오래된 내용으로 덮어써서 사라지지 않게 하기 위해서입니다.
요약
지금까지의 설계를 저 나름의 원칙으로 정리합니다.
- 한 번에 모든 것을 맡기지 않고, 공정을 나누고, 공정별로 부여하는 권한을 최소화합니다.
- 문맥이 필요한 수정은 같은 세션에서, 독립적인 판단은 다른 세션과 다른 모델로 진행합니다.
- AI의 판정은 형태를 정해서 받고, 문장을 해석하여 분기하지 않도록 합니다.
- 중단은 실패의 일종으로 이유별로 분류하고, AI 탓이 아닌 실패로는 시도 횟수를 줄이지 않습니다.
- 상태는 프로세스 외부에 두고, 어디서 멈춰도 이어서 재개할 수 있도록 합니다.
지난 글과 함께, AI에게 개발을 맡기는 시스템을 구축할 때 참고가 되기를 바랍니다.
개발팀에 AI 구동 개발 도입이나 업무 자동화에 대해 상담이 필요하시면, 프로필에서 연락 주십시오.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기