
내가 직접 작성한 규약에 내가 처음으로 걸려들다 ── 검증 4종의 자율 모드를 C3 본체에 구현한 밤 (v2.49.0)
요약
자작 프레임워크 C3(Claude Code Conductor) v2.49.0 릴리스와 함께 자율 모드 구현 과정을 다룹니다. LLM의 판단이 아닌 기계적 스크립트를 통해 자율 모드 선언의 유효성을 검증하는 fail-closed 설계 방식을 소개합니다.
핵심 포인트
- 자율 모드 선언의 유효성을 전용 스크립트로 기계적 검증
- 샌드박싱을 통한 플랜 파일 경로의 안전성 확보
- 유효하지 않은 선언 시 통상적인 인간 승인(HITL)으로 전환되는 설계
- 자율 모드가 스스로의 규약을 작성하는 구현 과정 기록
이전 기사: https://zenn.dev/satoh_y_0323/articles/d33df6623ceb75
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/
본 기사의 범위: 지난번 "릴리스는 하나도 없습니다"라고 썼던 자율 모드를, 이번에는 자작 프레임워크 C3 (Claude Code Conductor) 본체에 구현하여 v2.49.0을 릴리스한 하룻밤의 기록입니다. 다만 주인공인 skill 본체는 이번에도 배포 대상 외 (이유는 본문에서). 구현 그 자체보다, 구현 중에 일어난 부수적인 결과──규약을 작성한 본인이, 방금 작성한 규약 위반으로, 방금 만든 검증에 걸려든 이야기가 본 기사의 주인공입니다.
시작하며 ── "구현 여부는 실제 수요가 나타난 뒤에"라는 답 맞추기
이전 기사는 이렇게 마무리했습니다. "이 계층을 C3 본체에 포함할지는, 평소처럼 실제 수요가 나타난 뒤에 결정하겠습니다".
실제 수요는 5일 만에 찾아왔습니다. 검증 4종을 통해 쌓은 설계 판단과 격차 기록 31개 항목을 .dev/ (배포 대상 외의 검증 보관소)에 방치해 두면, 다음에 자율 모드를 사용하고 싶어진 순간에 다시 "세션 기록의 경위를 다시 읽고 복원"해야 하게 됩니다. 검증에서 가장 값진 교훈이 "운전 모드는 대화가 아니라 기계 판독 가능한 필드(machine-readable field)로 계승한다"였는데, 그 운전 규약 자체가 파일 계약(file contract)이 되어 있지 않았다──이는 구현하는 측의 결핍입니다.
그래서 이번에 한 일은, 자율 모드의 규약 그 자체를 skill로서 작성하는 것이었습니다. 승인 게이트(approval gate)를 어떤 객관적 조건으로 교체할지에 대한 대응표, 운전 모드를 선언하는 "모드:" 행의 문법, 선언이 진짜인지 판정하는 검증. 지난번 "검증 방법"이었던 것을, 이번에는 "배포물의 설계"로 격상했습니다.
그리고 이 구현 작업 자체를 자율 모드로 운전했습니다. 자율 모드가, 자율 모드의 규약을 작성한 셈이 됩니다.
무엇을 만들었나 (30초 버전)
세션 파일의 한 줄이 모든 기점입니다.
모드: 자율 plan=C:\...\.claude\plans\{위임플랜}.md cycles=E/3
이 행이 유효한 자율 선언일 때만, 워크플로우의 승인 게이트 10개가 객관적 조건으로 교체됩니다.
| 게이트 | 통상 (HITL) | 자율 모드 |
|---|---|---|
| 리포트 승인 | AskUserQuestion | 요점 제시 후 자동 진행 |
| ... | 비가역 조작·정보 부족 질문·usage 확인 | 인간 |
| 인간 그대로 (이 부분은 절대로 교체하지 않음) |
"유효한 자율 선언"의 판정은 3가지 조건──① 행이 모드: 자율 로 시작함 ② plan=이 있음 ③ 그 경로가 허가된 루트 ( ~/.claude/plans/ )에 갇혀 있는지 검사(sandboxing)를 통과하여 실재함. 하나라도 부족하면 무효 = 통상의 인간 승인 (HITL)으로 떨어지는 fail-closed 설계입니다. 판정은 LLM의 육안이 아니라, 전용 스크립트 mode_line.py를 실행하여 VALID / INVALID와 exit code로 기계적으로 수행합니다 (이 문장이 나중에 중요해집니다).
배포의 이중 구조
재미있는 점은 여기서, 이 skill 본체는 git에도 wheel에도 들어있지 않습니다. 배포물에 넣은 것은 dev-workflow와 init-session에 대한 "모드 행이 있으면 skill을 보러 간다"라는 연결점뿐입니다. 이용 측에는 skill이 존재하지 않으므로 판정은 항상 무효 = 완전한 기존 동작으로 거동 변화 제로입니다. 우선 자신의 환경에서 운용 실적을 쌓고, 숙성되면 제외된 3줄을 삭제하여 정식 배포로 전환합니다 (가역적). "배포물의 리포지토리에, git 추적을 받지 않는 파일로 기능을 구현한다"라는, 작성하면서도 조금 신기한 구성이 되었습니다.
실전 데이터 ── 구현 7개 태스크 + 리뷰 4회전
밤 22시가 넘어 착수하여, 새벽 릴리스까지 약 3시간. 구현 7개 태스크 (skill 신규 생성·연결점 2곳·배포 제외 3개 파일 동기화·테스트·wheel 실체 검증·상호 참조)를 TDD로 밀어붙이며, 리뷰 루프에 들어갔습니다.
| 회차 | code-reviewer | security-reviewer | 주요 내용 |
|---|---|---|---|
| 1 | High 1 / Med 2 / Low 4 | Med 1 / Low 3 | 총 11건 전건 대응 후 반려 |
| ... | 신규 Low 2 | 시정이 만든 새로운 허점을 포착 | |
| 4 | 0건 | 0건 | High/Medium 0으로 수렴 |
테스트는 1913 → 1919건 (전부 통과/All Green). 이 과정에서 인간(나)이 한 일은 시작을 알리는 한마디, 검색 인덱스 업데이트 확인 1회, 완료 후 백로그 대조, 그리고 릴리스 지시뿐입니다.
지난번에도 썼던 "지적 사항은 단조롭게 감소하지 않는다"가 이번에도 재현되었습니다. 게다가 이번에는 줄어들기는커녕, 회차 3의 Medium은 회차 2의 수정이 새롭게 만들어낸 허점입니다. 이 연쇄가 본 기사의 하이라이트 그 자체이므로, 순서대로 작성하겠습니다.
하이라이트 1 ── 로컬은 전원 통과, CI에서는 확실히 실패
회차 1의 High는 다음과 같았습니다. "git 추적 대상인 테스트가 git 비추적 (gitignored) 모듈에 의존하고 있다. 개발 환경에서는 전원 통과하지만, CI의 클린 체크아웃 (clean checkout) 시에는 확실히 FileNotFoundError 발생".
원인은 배포의 이중 구조 그 자체에 있었습니다. 모드 라인 판정 스크립트인 mode_line.py는 skill의 일부이므로 git 비추적 대상입니다. 그런데 이를 테스트하는 함수를 tests/ (git 추적 및 CI 실행 대상)에 작성했습니다. 개발 환경의 디스크에는 파일이 실제로 존재하기 때문에 테스트는 통과합니다. 하지만 CI가 프레시 클론 (fresh clone)을 하는 순간, 존재하지 않는 파일을 import 하려다 확실히 실패하게 됩니다.
더 골치 아픈 점은, 이 모순은 테스트를 아무리 돌려도 검출할 수 없다는 것입니다. tester는 2회 모두 "전체 테스트 통과"라고 올바르게 보고했으며, 거짓은 어디에도 없었습니다. "로컬의 통과"와 "CI의 통과"가 별개가 되는 구성 변경을 했음에도, 검증은 전자의 상태만 보고 있었던 것입니다. 이를 잡아낸 것은 리뷰어였으며, git ls-files (추적 0건)와 CI 정의 (클린 체크아웃)를 대조한 구성의 추론이었습니다. 테스트는 실행 환경의 수만큼 진실이 존재한다──"전원 통과"는 분포의 한 점에 불과하다는 교훈입니다.
나아가 회차 3에서는 반대쪽도 공격받았습니다. skip 가드를 넣은 symlink 위장 테스트가 "개발 환경 (Windows 비특권)에서는 symlink를 생성할 수 없어 skip, CI에서는 파일 부재로 skip──이 테스트가 실제로 PASS 하는 경로가 현재 제로"라는 지적이었습니다. 존재함에도 한 번도 실행되지 않는 테스트는, 실행되지 않는 monitoring과 마찬가지로 안도감만을 제공할 뿐입니다. 최종적으로 관리자 권한이 필요 없는 NTFS 정션 (junction)으로의 폴백 (fallback)을 구현하여, 개발 환경에 실제 PASS 경로를 만들어 해결했습니다.
하이라이트 2 ── "고쳐라"라는 문구는, 3회차를 돌고 나서야 계약이 되었다
회차 1에서 security-reviewer가 지적한 Medium은 다음과 같습니다. "봉쇄 검사 (realpath 정규화 + 허용 루트 판정)가 SKILL.md의 문장 기술로만 존재하며, 실행하는 메커니즘이 없다. symlink 해결은 파일 시스템에 대한 실제 액세스가 필요한 작업이므로, 텍스트를 읽는 LLM의 육안 판단으로는 원리적으로 실행할 수 없다".
타당한 지적이었기에 회차 2에서 수정했습니다. "mode_line.py를 실제로 호출하여 판정할 것. 육안으로 대체하지 말 것"이라고 규약에 명시했습니다. 그랬더니 회차 3에서 다음과 같은 지적을 받았습니다. "호출하라고 적었지만, 해당 스크립트에 CLI 진입점 ( main )이 없다. 호출 방법 (인자, 출력 형식)이 정의되지 않은 채 문구만 들어갔다".
과연 그렇다 싶어 CLI를 구현하고 호출 예시를 규약에 적었더니, 이번에는 code-reviewer에게 지적을 받았습니다. "그 호출 예시 <<< "{모드 라인}"은, 신뢰 경계의 텍스트를 더블 쿼트 (double quote) 내에 구문적으로 임베딩(embedding)하고 있다. 이 셸은 더블 쿼트 내에서도 $(...)를 평가하므로, 모드 라인에 조작이 있다면 스크립트가 실행되기 전에 셸 레벨에서 명령어가 실행된다"──실제로 주입 페이로드 (injection payload)를 흘려보내 실증까지 마친 상태였습니다. 최종 형태는 "grep으로 세션 파일에서 직접 파이프 (pipe)로 흘려보내는" 방식이 되었습니다. 커맨드라인에 나타나는 리터럴은 파일 경로뿐이며, 신뢰할 수 없는 텍스트는 데이터로서만 셸을 통과하게 됩니다.
돌이켜보면 이 3회차는 '문언(文言) 수정 → 호출 가능(契約) → 안전하게 호출 가능(呼び出し方)'로 단계적으로 깊이 파고들었습니다. 어떤 회차의 수정도 선의였지만 정확했고, 그리고 이전 수정이 다음 허점을 만들었습니다. 지난 기사에서 '수정 자체가 새로운 설계이기 때문에 지적이 늘어난다'고 썼는데, 이번에는 증가하는 양까지 관찰할 수 있었습니다. '모두 대응한다'를 고정한 객관 조건 게이트(objective condition gate)였기 때문에, 이 연쇄는 중간에 '뭐 괜찮겠지'로 무너지지 않고 4회차의 0건까지 끝까지 달릴 수 있었다는 점은 강조하고 싶습니다.
덧붙여 솔직한 기록을 하나 남깁니다. 이 'CLI 계약이 없다'는 최상류 원인은, 수정 계획을 작성한 부 Claude(나)가 문언 지시만으로 태스크를 종료했기 때문입니다. C3는 tier-routing 학습을 위해 각 게이트의 성공 여부를 역할별로 기록하고 있으므로, 이번 회차의 실패는 planner의 failure로서 스스로 기록했습니다. 자율 루프 안에서도 누가 실수했는지 모호하게 하지 않는 것이 나중에 도움이 됩니다.
절정 3 ── 그리고, 자신이 잡히다
마지막 수정으로 호출 예시와 같은 grep 파이프를 실제 데이터, 즉 현재 내가 구동에 사용하고 있는 세션 파일을 향해 동작 확인했습니다. 기대했던 것은 VALID였습니다. 돌아온 것은:
INVALID not_found
버그인가 싶어 조사한 개발 에이전트의 보고가 훌륭했습니다. '이것은 파이프 메커니즘의 결함이 아니라, 이 세션 파일의 모드 라인 내용에 따른 올바른 fail-closed 판정이다.'
제가 작성한 모드 라인은 다음과 같았습니다.
모드: 자율 plan=C:\...\dazzling-cooking-dusk.md(루프 내장 배포물화. 위임 플랜 승인됨...
경로 바로 뒤에 공백 없이 주석의 괄호를 연결했습니다. BNF 상 여기는 plan-path의 연속이므로, 판정기는 '…dusk.md(루프 내장…'이라는 파일을 찾으러 갔고, 실재하지 않음 → not_found → 무효였습니다. 규약의 문법을 작성한 본인이 그 규약에 위반하는 라인으로 자신의 환경을 구동하고 있었던 것입니다. 게다가 몇 시간 전까지 이 라인은 누구에게도 읽히지 않는(읽는 메커니즘이 없는) 단순 메모였기 때문에, 위반은 어디에서도 보이지 않았습니다. 검증을 구현한 순간, 첫 번째 먹잇감으로 자신이 걸린 셈입니다.
라인을 고치고(주석은 다른 라인으로 분리), 제대로 VALID가 돌아와 exit 0이 되었습니다. init-session이 세션 복원 시 모드 라인을 검증하는 흐름이 실제 데이터에서 일관되게 작동한 순간이기도 했습니다.
지난 기사의 절정 4를 기억하는 분들은 미소지을지도 모릅니다. 그때 '세션의 세대교체로 자율 모의 합의가 상실된' 사고로부터 '모드: 라는 기계 가독 필드를 두자'는 대책이 나왔습니다. 이번에 잡힌 것은 바로 그 대책의 라인입니다. 필드를 발명하는 것만으로는 부족하고, 문법을 정하고, 검증을 구현하며, 검증을 자신에게 처음 향하게 해야 '기계 가독(machine-readable)'이 진짜가 됩니다. 인간이 작성하는 YAML이나 커밋 메시지에 linter가 필요한 것과 같은 일이 LLM의 구동 기록에서도 일어난다고 생각합니다.
업무/개인 개발에 활용할 수 있는 점
1. 도그푸딩(Dogfooding)의 첫 번째 먹잇감은 자신이라고 생각하라
만든 검증/가드/linter는 우선 자신의 기존 데이터에 적용해야 합니다. 이번에 발견된 것은 타인의 위반이 아니라 자신의 위반이었습니다. 역설적으로, 실제 데이터에 적용하기 전까지 그 검증은 작동 실적이 0입니다. '규약을 작성한다'와 '규약이 집행되고 있다' 사이에는 문법 정의 → 기계 검증 → 실제 데이터에서의 첫 탐지가라는 단차가 3가지 있었습니다.
2. '…하는 것(〜すること)'이라는 문언은 방어가 아니다
'봉쇄 검사를 실시하는 것'은 실행 주체, 호출 방식, 실패 시의 동작이 결정되어야 비로소 계약이 됩니다. 문언 → 실행 가능한 진입점 → 안전한 호출 방식으로 이번에 3회차를 거쳤습니다. 리뷰에서 '이 규약, 누가 어떻게 실행해?'라고만 물어봐도 2회분은 앞당길 수 있었을 것입니다.
3. '전 테스트 녹색(全テスト緑)'은 실행 환경의 한 지점일 뿐이다
git 추적과 gitignored가 혼재하는 구성에서는, 로컬의 녹색, CI의 녹색, 배포처의 녹색은 별개입니다. 구성을 바꾸는 변경(추적 범위/제외 패턴/의존성 배치 위치)이 생기면, '어떤 환경의 녹색을 확인했는지'를 수용 조건에 명시해야 합니다. skip만 가득해서 사실 한 번도 돌아가지 않은 테스트도 같은 죄입니다.
4. 숙성에는 '배포하지 않는다'는 선택지가 있다
새로운 메커니즘을 첫 번째 릴리스부터 배포물에 포함할 필요는 없습니다. 이번에는 접속점(무효할 경우 기존 동작으로 떨어지는 fail-closed 분기)만 배포하고, 본체는 수중에 두고 숙성시키는 구성으로 했습니다. 전환은 제외할 3줄을 삭제하는 것뿐이라는 점을 기계 테스트(Machine Test)로 담보해 두었습니다. "넣을 것인가 말 것인가"라는 이지선다에 "입구만 넣고 내용은 기다린다"를 추가하면, 릴리스의 심리적 허들이 상당히 낮아집니다.
요약
- 지난번 "릴리스 0건 · 검증 4건"이었던 자율 모드를 규약 skill(게이트 대응표 · 모드 문법 · 기계 검증 CLI)로서 C3 본체에 구현하여 v2.49.0을 릴리스. skill 본체는 배포 대상에서 제외한 채 수중에서 숙성시키는 이단계 전략을 채택 - 구현 작업 자체를 자율 모드로 운전하며, 리뷰 4회전 · High/Medium 0으로 수렴. "고쳐라(문구) → 호출할 수 있다(계약) → 안전하게 호출할 수 있다(호출 방식)"와 같이 시정이 한 단계씩 심화되는 연쇄를, 객관적 조건 게이트(Objective Condition Gate)가 중간 이탈 없이 완주함
- 최대의 부산물은, 완성된 검증을 실제 데이터에 적용하는 순간 자신의 모드 행(mode line)이 BNF 비준수임을 발견한 것. 규약은 작성한 시점에는 존재하지 않으며, 기계 검증이 자신을 포함한 모두를 향하는 시점에 비로소 존재함
지난번 맺음말을 빌리자면, "무엇이 사람만이 할 수 있는지가 서면으로 작성되어 있으므로 안전하다"라는 서면이, 이번에 드디어 기계가 읽을 수 있는 서면이 되었습니다. 그리고 그 검증의 첫 업무가 작성자 본인을 적발한 것이라는 점이, 이 밤의 가장 솔직한 성과 보고입니다.
C3를 써보고 싶다면 ── 시작하는 법
pip install claude-code-conductor # C++ 컴파일러 불필요
cd your-project
c3 init # .claude/ 에 에이전트 정의 · skill · hook 가 전개됨
그다음은 Claude Code에서 /start
.
본 기사의 자율 모드 skill 본체는 아직 배포물에 포함되어 있지 않지만(숙성 중), 그 골격인 Approval Flow · design-critic 감사 · 리뷰 반복은 표준 워크플로우에서 그대로 체험할 수 있습니다. 어댑터는 Claude / Codex / Cursor / OpenCode 4종에 대응합니다 (c3 init --platform ...).
"여기서 막혔다" "이 부분이 이상하다" ── 막힌 지점에 대한 보고가 가장 감사합니다. Issue나 PR로 편하게 남겨주세요.
링크
- C3 GitHub: https://github.com/satoh-y-0323/claude-code-conductor
- C3 PyPI: https://pypi.org/project/claude-code-conductor/
- C3 공식 문서: https://satoh-y-0323.github.io/claude-code-conductor/
- v2.49.0 릴리스 노트: https://github.com/satoh-y-0323/claude-code-conductor/releases/tag/v2.49.0
- 이전 기사 (『승인 버튼의 수신처를 사람에서 「지적 0」으로 교체했다』 / 검증 4건의 실전 기록): https://zenn.dev/satoh_y_0323/articles/d33df6623ceb75
- 7/5 첫 위임 기사 (『지우는 메커니즘은 처음부터 있었다』 / C3 v2.45.0): https://zenn.dev/satoh_y_0323/articles/e4880ffbbc9ed8
Discussion

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