팀 하네스 설계기 3편: 서브에이전트·스킬·훅 해부
하네스 감사를 돌리다가 가장 섬뜩했던 발견은 위험한 명령이 아니었다. 리뷰 지적을 받은 수정 에이전트가 코드 대신 규칙을 고치면, 리뷰는 통과한다는 사실이었다. 커버리지 기준을 낮추거나, 체크스타일 룰을 지우거나, 리뷰 규칙 문서를 편집하면 된다. 그날부터 훅의 역할을 "위험 명령 차단"에서 "게이트 자체를 지키는 것"으로 다시 정의했다.
1편에서 설계 결정을, 2편에서 실제 실행 흐름을 다뤘다. 마지막 편은 부품 해부다. kc-admin 하네스의 디렉토리를 열면 이렇게 생겼다.
kc-admin/
├── CLAUDE.md # 30줄: 빌드 명령 + 절대 규칙 4개 + "해당 작업을 할 때 읽어라" 목록
├── .mcp.json # atlassian, figma (둘 다 http MCP)
└── .claude/
├── settings.json # 권한 allow + 훅 6종 연결
├── rules/ (9) # architecture, conventions, code-convention, boot4-java25,
│ # ops-safety, security, ssot-workflow, fe-collaboration, git-convention
├── agents/ (7) # implementer, reviewer, fixer, doc-analyst, ssot-checker, ssot-reviewer, mock-writer
├── skills/ (6) # ssot-creator, ssot-driven-dev, ssot-driven-mock-document, ssot-followup,
│ # onboarding, harness-scorecard
├── hooks/ (6) # bash-guard, file-guard, mcp-guard, secret-scan, lint-mark, lint-check
└── worktrees/ # 티켓·parent 워크트리 (gitignore)
1. CLAUDE.md는 30줄이다: 지연 로딩되는 규칙
CLAUDE.md는 세션마다 컨텍스트에 통째로 들어간다. 그래서 여기에는 모든 작업에 항상 필요한 것만 둔다. 빌드·실행 명령, 그리고 절대 규칙 4개.
- SSOT 게이트: 승인된 SSOT 문서 없이 구현 금지, 원문 재해석 금지.
- 아키텍처 경계: domain은 순수 Java, application은 adapter를 모른다.
- TDD + 리뷰 파이프라인: 기능 구현은
/ssot-driven-dev로만. - 추측 금지: 모호하면
[OPEN]으로 올리고 기획자에게 묻는다.
나머지는 "상세 규칙 (해당 작업을 할 때 읽어라)" 목록으로 9개 규칙 파일을 가리키기만 한다. 262줄짜리 ssot-workflow.md, 182줄짜리 conventions.md를 매 세션 다 싣는 대신, 그 작업을 할 때만 읽게 한다. 규칙 파일 합계가 900줄이 넘는데도 상시 컨텍스트는 30줄이다.
2. 서브에이전트 7종: 만드는 자와 검증하는 자를 가른다
| 에이전트 | 모델 | 도구 | 역할 | 금지된 것 |
|---|---|---|---|---|
implementer | opus | Read·Edit·Write·Bash·Jira MCP | 작업 티켓 1개의 사전 점검 ~ 완료 검증을 TDD로 수행 | SSOT 수정, 커밋, 리뷰, 티켓 상태 전환, 브랜치 생성 |
reviewer | opus | Read·Grep·Glob·Bash | diff를 SSOT와 대조해 적대적 리뷰 | 코드 수정 (Bash는 git 읽기와 테스트 실행까지) |
fixer | sonnet | Read·Edit·Write·Bash | finding을 검증해 정당한 것만 최소 수정 | 리뷰어를 만족시키려 멀쩡한 코드를 바꾸는 것 |
doc-analyst | opus | Read·Atlassian·Figma MCP | 문서 1개를 분석해 출처 달린 facts 반환 | 파일 생성·수정, SSOT 작성 |
ssot-checker | sonnet | Read·Grep·Glob | SSOT 초안의 형식만 검사 | 원문 대조, 수정 |
ssot-reviewer | opus | Read·Atlassian·Figma MCP | 원문과 대조해 7항목 rubric 채점 | SSOT 수정 |
mock-writer | sonnet | Read·Write·Edit (Bash 없음) | 담당 컨트롤러 1개의 mock API 문서 생성 | main 소스 변경, 다른 노드 소유 파일 수정 |
모델 배정 기준은 단순하다. 원문을 해석하거나 결함을 찾는 판단 노드는 opus, 정해진 체크리스트나 받은 지시를 수행하는 노드는 sonnet. fixer가 sonnet인 이유는 판단을 reviewer가 이미 했고, fixer는 그 판단을 검증해 최소 수정만 하기 때문이다.
reviewer: "결함이 있다고 가정하라"와 "지어내지 마라"를 동시에
reviewer 정의의 첫 문장은 공격적이다.
너는 코드 리뷰어다. 이 diff에는 결함이 있다고 가정하고 찾아라. 구현자의 설명은 주어지지 않는다. 코드와 SSOT 문서만이 사실이다.
그런데 finding 자격 절에서 균형을 잡는다.
- 구체적 실패 시나리오가 필수다. "이 입력/상태 → 이 동작 → 이 잘못된 결과"를 못 쓰면 버린다.
- 스타일·취향·"이렇게도 할 수 있다"는 리뷰 대상이 아니다.
- 확실하지 않으면
[PLAUSIBLE]로 표시한다. - 결함이 없으면 "결함 없음"이라고 답하라. 억지로 만들어내는 것이 최악의 실패다.
검사 순서는 우선순위대로 9개다. SSOT 대조, AC ↔ 테스트 전수 매핑, 테스트 진정성, 아키텍처 경계, 정확성, 보안, 문서화, 운영 리스크, 그리고 마지막이 빌드 게이트 변경이다. diff에 build.gradle, config/checkstyle/**, .gitlab-ci.yml 변경이 있으면 SSOT 근거가 없는 한 BLOCKING이다. 커버리지 minimum을 낮추거나 룰을 지워서 finding을 "해소"하는 경로를 reviewer가 마지막으로 막는다.
테스트 진정성 항목도 눈여겨볼 만하다. assertion 없는 테스트, 커버리지만 채우는 테스트, 구현을 베낀 동어반복 테스트, 그리고 간헐 실패를 덮는 패턴(@Disabled, 재시도 루프, Thread.sleep 추가, 검증 완화)은 BLOCKING이다. 라인 커버리지 100% 게이트가 있으면 에이전트는 커버리지를 "채우는" 방향으로 최적화하기 쉽다. 그 유혹을 리뷰가 잡는다.
fixer: 검증 먼저, 수정은 그다음
fixer는 finding을 받으면 고치기 전에 실패 시나리오가 실제로 성립하는지부터 확인한다. 성립하면 재현 테스트를 먼저 추가하고 최소 수정, 성립하지 않으면 REJECTED와 근거를 남긴다. 보안 finding도 같은 절차라서, 인가 누락이면 "인가 없는 요청 → 401/403" 테스트가 먼저 생긴다. 이 출력이 그대로 리뷰 로그에 기록된다.
프롬프트에는 파라미터만
스킬이 서브에이전트를 부를 때 지키는 규칙이 하나 있다. 노드의 행동은 에이전트 정의 파일이 담당하고, 프롬프트에는 파라미터만 넣는다. implementer에게 넘기는 것은 워크트리 경로, 브랜치명, 하네스 루트 경로, parent 워크트리 경로, 티켓 ID, SSOT 경로, 담당 AC, 파생 원점이 전부다. 오케스트레이터의 해석을 덧붙이면 노드마다 행동이 미묘하게 달라지고, 리뷰 독립성도 깨진다.
경로를 세 개나 넘기는 데도 이유가 있다. 규칙은 하네스 루트에서, SSOT는 parent 상주 워크트리에서, 코드는 티켓 워크트리에서 읽는다. 티켓 워크트리 안에도 규칙과 SSOT 사본이 있지만, 파생 시점의 고정본이라 그 사이 반영된 [ANSWERED]나 강화된 규칙이 빠져 있다.
3. 스킬 6종: 오케스트레이터는 사람이 부른다
| 스킬 | 역할 | 호출 방식 |
|---|---|---|
/ssot-creator | 문서 → SSOT, rubric 채점, Jira 연동, 갱신 모드, 승인 처리 | 수동 전용 |
/ssot-driven-dev | SSOT → TDD 구현, 리뷰, .http, 병합, 시퀀스 다이어그램 게시 | 수동 전용 |
/ssot-driven-mock-document | 구현 전 프론트에 mock API 계약 선전달 (예외 경로) | 수동 전용 |
/ssot-followup | 개발 완료 후 이슈를 A·B·C·D로 분류해 기존 경로로 라우팅 | 수동 전용 |
/onboarding | 하네스 문서를 근거로 개발 절차 안내, 읽기 전용 | 모델 자동 호출 허용 |
/harness-scorecard | 자가 채점 시트 재진단 | 봉인 |
오케스트레이터 스킬 4종에는 전부 disable-model-invocation: true가 붙어 있다. 모델이 "이 상황엔 이 스킬이 맞겠다"고 판단해 스스로 부를 수 없고, 담당자가 슬래시 커맨드로 불러야만 로드된다. Jira 티켓을 만들고, 브랜치를 파고, 병합까지 하는 스킬이 대화 흐름에 따라 자동으로 끼어드는 것은 원하지 않았다.
이 결정에는 부작용이 하나 있었다. 새 세션을 열고 "승인"이라고만 말하면 스킬이 로드되지 않은 상태라, 모델이 승인 절차(채점 확인, 중복 생성 방지, 티켓 ID 역기입, 커밋)를 빼먹고 즉흥적으로 처리한다. 그래서 /ssot-creator KA-123 승인처럼 승인을 인자로 받아 승인 처리 노드로 직행하는 진입점을 따로 만들었다.
/harness-scorecard는 한 단계 더 잠갔다. disable-model-invocation: true에 user-invocable: false까지 걸어서 사람도 슬래시 커맨드로 부를 수 없다. 관리자가 프론트매터를 직접 true로 바꿔야 실행되고, 실행이 끝나면 스스로 다시 봉인한다. 하네스를 채점하는 스킬이 아무 때나 돌면서 점수를 갱신하면, 점수가 개선의 근거가 아니라 소음이 된다. 쓰기 대상도 채점 시트 파일 하나와 자기 프론트매터 한 줄로 제한했다. 감점을 발견해도 이 스킬은 하네스를 고치지 않는다.
반대로 /onboarding만은 자동 호출을 허용했다. 새로 들어온 개발자가 "이 프로젝트 개발 절차 알려줘"라고 물으면 바로 떠야 하고, 읽기 전용이라 부작용이 없다. 답변 근거는 하네스 파일뿐이고, [OPEN]·AC·파생 원점 같은 내부 용어는 처음 나올 때 한 줄로 풀어 설명하게 했다.
4. 훅 6종: 문서 규칙을 기계가 집행하는 자리
settings.json의 훅 연결은 이렇다.
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "command": ".claude/hooks/bash-guard.py" }] },
{ "matcher": "Read|Edit|Write|NotebookEdit|Grep", "hooks": [{ "command": ".claude/hooks/file-guard.py" }] },
{ "matcher": "mcp__atlassian__.*|mcp__plugin_atlassian_atlassian__.*|mcp__figma__.*|mcp__plugin_figma_figma__.*",
"hooks": [{ "command": ".claude/hooks/mcp-guard.py" }] }
],
"PostToolUse": [
{ "matcher": "Edit|Write|NotebookEdit",
"hooks": [{ "command": ".claude/hooks/secret-scan.py" }, { "command": ".claude/hooks/lint-mark.py" }] }
],
"Stop": [
{ "hooks": [{ "command": ".claude/hooks/lint-check.py", "timeout": 300 }] }
]
}
}PreToolUse 훅 세 개가 실행 전에 막고, PostToolUse 훅 두 개가 쓰기 직후에 검사·기록하고, Stop 훅 하나가 턴이 끝날 때 한 번 몰아서 린트를 돈다. 판정은 전부 permissionDecision의 deny·ask·allow 셋 중 하나로 낸다.
bash-guard.py (765줄): 명령 원문 스캐너
가장 큰 훅이다. 하는 일을 세 갈래로 나누면 이렇다.
| 판정 | 대상 |
|---|---|
| deny | 광역 재귀 삭제, sudo, 디스크 파괴, 원격 스크립트 파이프 실행, git push --force(--force-with-lease는 허용), 자격증명 경로 접근, .git/hooks·core.hooksPath 지속성 벡터, 가드레일 파일의 셸 경유 변조, refspec으로 dev·live·master 직접 갱신 |
| ask | allowlist 밖의 git -C 서브명령, 하네스 루트에서 pathspec 없는 커밋·add -A, git stash 전 형태, branch -D(티켓 브랜치 패턴 제외), worktree add의 경로가 .claude/worktrees/ 밖, 병합 전략 옵션 -X·-s, printenv |
| allow | 워크트리 안 src/ 하위 파일의 rm·mv(mock 정리·리팩터링), mockapi 디렉토리 복사 같은 스킬의 정규 경로 |
몇 가지 설계 포인트를 짚는다.
settings의 프리픽스 allow는 너무 넓다. Bash(git -C:*)를 허용하면 git -C <경로> push --force도, reset --hard도 통과한다. 그래서 훅이 -C 뒤의 서브명령을 다시 allowlist와 대조한다. 권한 설정이 1차로 넓게 열고, 훅이 2차로 좁힌다.
자동 병합 allowlist를 훅이 집행한다. merge는 .claude/worktrees/ 안의 워크트리에 에픽 parent 브랜치가 실제로 체크아웃돼 있고(git symbolic-ref로 실측), 같은 에픽의 티켓 브랜치를 병합할 때만 무프롬프트다. 1편에서 말한 채점 시트의 "allowlist 없는 자동 병합 금지" 항목이 문서가 아니라 이 코드로 만족된다.
ask는 즉시 결정하지 않고 보류한다. git -C wt push origin x; rm -rf / 같은 복합 명령에서 앞 세그먼트가 ask라고 바로 ask로 끝내면, 뒤의 deny가 사람 확인 한 번으로 통과된다. 그래서 모든 세그먼트를 다 본 뒤 deny가 하나라도 있으면 deny가 이긴다.
세그먼트 분리는 따옴표를 존중한다. 처음에는 ;, &, |로 단순 분리했더니 커밋 메시지 안의 괄호와 파이프가 오판을 냈다. 지금은 따옴표 밖의 구분자만 보고, -- pathspec 판정은 shlex 토큰 기준이다.
예외는 fail-closed다. 판정 도중 예기치 않은 오류가 나면 통과시키지 않고 ask로 결정한다. 훅이 조용히 죽으면 settings의 프리픽스 allow가 단독으로 열리기 때문이다.
그리고 이 훅은 자기 한계를 docstring에 명시한다.
이 가드는 명령 "원문" 스캐너이지 샌드박스가 아니다.
python3 -c같은 인터프리터 인라인 실행이 런타임에 조립하는 동작은 원문에 드러나지 않으면 잡지 못한다. 근본 방어는 인터프리터 인라인 실행에 광역 allow를 주지 않는 것이다.
실제로 전역 설정에 있던 Bash(python3:*) allow는 하네스 감사에서 제거됐다. 훅은 최후 방어선이 아니라 흔한 실수와 직접 변조를 걸러 내는 1차 필터다. 방어선의 한계를 코드 옆에 적어 두는 것이 다음 사람이 이 훅을 과신하지 않게 하는 가장 싼 방법이다.
file-guard.py: 규칙을 고쳐서 통과하는 경로를 막는다
Read·Edit·Write·Grep 전부에 걸린다.
# 1) 시크릿 파일: 모든 도구 접근 거부 (.env 계열·키/인증서·SSH 개인키·~/.aws 등)
# 2) 쓰기 도구 한정
if re.search(r"(^|/)\.git(/|$)", norm):
decide("deny", ".git 내부 직접 쓰기는 금지됩니다 (훅 심기 등 지속성 벡터).")
if re.search(r"\.claude/(settings\.json|settings\.local\.json|hooks(/|$))", norm):
decide("ask", "가드레일 파일(.claude 설정/훅) 수정입니다.")
if re.search(r"(^|/)(CLAUDE\.md|\.mcp\.json)$", norm) or re.search(r"\.claude/(rules|skills|agents)(/|$)", norm):
decide("ask", "하네스 게이트 본체 수정입니다. 규칙을 고쳐 finding 을 해소하는 경로를 막기 위해 사용자 확인이 필요합니다.")
if BUILD_GATE_RE.search(norm) or CHECKSTYLE_DIR_RE.search(norm):
decide("ask", "기계 게이트 파일 수정입니다. 커버리지·린트·CI 게이트 값을 낮춰 finding 을 해소하는 경로를 막습니다.")이 글 맨 앞의 섬뜩한 발견에 대한 처방이 이 몇 줄이다. 규칙 문서와 빌드 게이트 파일은 deny가 아니라 ask다. 의존성 추가처럼 SSOT 근거가 있는 정당한 변경도 있기 때문이다. 대가로 하네스 정비 세션에서는 편집할 때마다 확인이 뜬다. docstring에 "의도된 마찰이며, 과하면 이 규칙 한 줄을 조정한다"고 적어 뒀다.
.env 차단에도 우회로가 있었다. path만 검사하니 Grep의 glob: ".env*" 인자로 내용을 조회할 수 있었다. 지금은 glob 인자도 같은 패턴으로 검사한다.
mcp-guard.py: MCP 쓰기에 최소 권한을
Bash와 파일은 훅이 지키는데 MCP 쓰기만 무검사 통과였다. 이 훅이 그 구멍을 메운다.
- 읽기 도구(
get*,search*,lookup*등)는 allow로 무프롬프트 통과시킨다. 쓰기는 무프롬프트인데 더 안전한 읽기가 매번 확인에 걸리는 역전을 막기 위해서다. - Jira 쓰기는 대상 이슈 키·프로젝트 키가
KA면 allow, 밖이면 ask. 정상 워크플로우는 막힘없이 돌고, 다른 프로젝트에 쓰는 순간만 사람이 본다. - Confluence 쓰기는 ask. 원문은 읽기 전용 입력이기 때문이다. 유일한 예외는 하네스가 게시하는 시퀀스 다이어그램이다. 지정된 다이어그램 폴더 하위 생성, 그리고 하네스가 레지스트리(
confluence-diagram.json)에 기록한 페이지의 갱신만 allow다. 하네스가 만든 페이지만 하네스가 고칠 수 있다. - Figma 쓰기는 전부 ask. 대상 범위를 좁힐 수단이 없다.
- 대상 판별은
issueIdOrKey,projectKey같은 대상 지정 필드만 본다. 댓글 본문에서 키를 긁으면 "BA-15 참고" 같은 언급만으로 오탐이 난다. 키를 못 찾으면 조용히 통과시키지 않고 ask다.
secret-scan.py: 쓰고 난 직후의 사후 검출
file-guard가 시크릿 파일 접근을 사전에 막는다면, secret-scan은 방금 쓴 내용에 시크릿이 들어갔는지 사후에 본다. 패턴은 두 층이다.
- 고신호 패턴: AWS 키, PEM 개인키 블록, GitHub·Slack·Anthropic·Google 토큰, 이 프로젝트가 쓰는 Mailgun 키 형식. 모든 파일에 적용하고 면제가 없다.
- 일반 패턴:
password,secret,api_key같은 키워드에 16자 이상 값이 붙은 형태.JWT_SECRET,jwtSecret,MAILGUN_API_KEY처럼 식별자에 접합된 형태까지 잡는다. 테스트 코드,.http, SSOT 문서의 예시 JSON은 오탐을 줄이려고 제외한다.
재밌는 오탐이 하나 있었다. 비밀번호 정책 정규식 상수 PASSWORD_PATTERN = "^(?=..."이 하드코딩 시크릿으로 잡혔다. 그래서 식별자가 PATTERN·REGEX로 끝나거나 값이 ^로 시작하거나 (?를 포함하면 일반 패턴에서만 면제한다. 고신호 패턴에는 여전히 면제가 없다.
lint-mark.py + lint-check.py: 기록은 매번, 검사는 한 번
편집할 때마다 checkstyle을 돌리면 느리다. 그래서 둘로 쪼갰다. PostToolUse의 lint-mark는 .java 파일이 바뀌면 경로를 마커 파일에 기록만 한다. 마커는 gradlew clean에 지워지지 않도록 build/가 아니라 .gradle/에 둔다. Stop 훅의 lint-check가 턴이 끝날 때 마커를 읽어, 저장소 루트별로 checkstyle을 한 번 돈다.
위반이 있으면 exit 2로 턴 종료를 막고 위반 목록을 Claude에게 피드백한다. Claude가 고치고 다시 끝내려 하면 다시 검사한다. 여기에 안전장치가 세 겹 붙어 있다.
- 연속 3회 차단 후에도 위반이 남으면 경고와 함께 통과. 무한 Stop 루프를 막는다. 최종 방어는 어차피 빌드 게이트다.
- 티켓 워크트리의 위반은 차단하지 않고 경고만. 그 워크트리는 백그라운드 implementer가 편집 중일 수 있다. exit 2로 오케스트레이터를 남의 워크트리 수정으로 유도하면 쓰기 경합이 난다. 차단은 오케스트레이터 자신의 편집(하네스 루트)으로 한정한다.
- gradle 빌드가 락을 잡고 있는 루트는 건너뛴다.
.gradle락 파일에 POSIXlockf논블로킹 시도를 해 보고, 잡혀 있으면 빌드 중으로 판정한다. 해당 파일은 마커에 남겨 다음 Stop에서 재검사한다.
린트 외의 실행 오류(gradle 환경 문제 등)는 fail-open이다. bash-guard가 fail-closed인 것과 반대다. 보안 훅은 의심스러우면 막고, 품질 훅은 의심스러우면 통과시키고 최종 게이트에 맡긴다. 두 훅의 실패 모드가 다른 것은 의도된 비대칭이다.
5. 문서 규칙과 기계 집행의 경계
규칙 파일 9종을 쓰면서 계속 물었던 질문이 있다. "이 규칙을 기계가 집행할 수 있는가?" 할 수 있으면 기계로 옮기고, 못 하면 reviewer 검사 항목으로 넣었다.
| 규칙 | 문서 위치 | 기계 집행 | 사람·LLM 방어 |
|---|---|---|---|
| 헥사고날 계층 import 경계 | architecture.md | 없음 (패키지 경계라 기계 검증 대상 밖) | reviewer 검사 4 (domain의 spring import 등 즉시 BLOCKING) |
| 금지 라이브러리 | conventions.md | checkstyle 금지 import: Jackson 2 databind·spring-retry·RestTemplate 등 | reviewer 검사 4 |
| 보안 (난수·약한 해시·SSL 우회) | security.md | checkstyle: java.util.Random·Math.random·MD5/SHA-1·TrustManager·프로세스 실행 금지 (main 소스 한정) | reviewer 검사 6 (계정 열거, IDOR, 타이밍 누출 등) |
| 운영 안전 (스레드·페이징) | ops-safety.md | checkstyle: Executors·@Async import 금지 (전 소스), 무페이징 findAll() 차단 (main 한정) | reviewer 검사 8 (트랜잭션 안 외부 I/O, N+1 등) |
| 테스트 커버리지 | conventions.md | JaCoCo 라인 100% 게이트 | reviewer 검사 3 (테스트 진정성) |
| 시크릿 금지 | security.md | file-guard + secret-scan | reviewer 검사 6 |
| 자동 병합 범위 | git-convention.md | bash-guard allowlist | 승격은 사람 |
| 빌드 게이트 보호 | 각 규칙 | file-guard ask | reviewer 검사 9 |
보안 룰 대부분과 findAll() 차단은 main 소스로 한정했다. 테스트가 검증 목적으로 findAll()을 부르거나 픽스처에 난수를 쓰는 것까지 막으면 정당한 코드가 깨진다. 포맷팅(들여쓰기·줄 길이)도 강제하지 않는다. 기계 집행은 범위를 정확히 좁혀야 신뢰를 얻는다. 그리고 checkstyle에 새 룰을 넣는 것은 문서화된 컨벤션에 근거가 있을 때만이다. 취향 룰은 금지다.
한 가지 더. 프로젝트 settings.json은 defaultMode: "default"다. 온보딩 문서는 --dangerously-skip-permissions로 세션을 열지 말라고 명시한다. bypass 모드에서는 훅의 ask 판정이 사람 확인 없이 지나간다. ask를 많이 쓰는 하네스일수록 권한 모드가 그 하네스의 일부다.
6. 시니어의 시선으로 정리하는 인사이트
① 서브에이전트 설계의 핵심은 "무엇을 못 하게 하는가"다
7종 에이전트 정의를 다시 읽어 보면 역할 설명보다 금지 목록이 길다. implementer는 커밋·리뷰·SSOT 수정을 못 하고, reviewer는 수정을 못 하고, mock-writer는 Bash조차 없다. 권한을 깎아야 역할이 생긴다. maker와 checker가 같은 도구를 가지면 분리는 이름뿐이다.
② 리뷰어에게는 공격성과 절제를 동시에 줘라
"결함이 있다고 가정하라"만 주면 지어낸 finding이 쏟아지고, fixer가 멀쩡한 코드를 고친다. "실패 시나리오를 못 쓰면 버려라", "없으면 없다고 답하라"를 같이 줘야 리뷰가 신호가 된다. 그리고 fixer에게 REJECTED할 권리를 줘야 그 균형이 완성된다.
③ 게이트는 게이트 자신도 지켜야 한다
가장 위험한 우회는 위험한 명령이 아니라 게이트 설정 변경이었다. 커버리지 기준, 린트 룰, 리뷰 규칙 문서. 에이전트가 자기 평가 기준을 고칠 수 있으면 그 기준은 없는 것과 같다. 규칙 파일과 빌드 게이트 파일을 훅으로 보호하고, reviewer가 한 번 더 본다.
④ 훅의 실패 모드는 목적에 따라 다르게 설계한다
보안 훅은 fail-closed, 품질 훅은 fail-open. 차단 훅에는 무한 루프 탈출구를, 병렬 환경에는 경합 회피를. 훅은 "막는다"보다 "언제 막지 않는가" 를 설계하는 데 시간이 더 들었다.
⑤ 방어선의 한계를 코드 옆에 적어라
bash-guard의 docstring은 "이것은 샌드박스가 아니다"로 시작하는 위협 모델 절을 가지고 있다. 무엇을 막지 못하는지, 근본 방어는 어디에 있는지. 다음에 이 훅을 고칠 사람(사람이든 에이전트든)이 과신하지 않게 하는 문장이다.
마무리: 하네스는 규칙의 모음이 아니라 집행의 구조다
한 문장으로 줄이면 이렇다. 규칙은 문서에 쓰고, 지켜야 하는 규칙은 기계로 옮기고, 기계가 못 지키는 규칙은 독립된 리뷰어에게 맡긴다. 그리고 그 셋 모두를 에이전트가 고칠 수 없게 잠근다.
3주 동안 26번의 하네스 커밋을 거치며 남은 교훈은 결국 하나다. 에이전트를 믿을 수 있게 만드는 것은 더 똑똑한 프롬프트가 아니라, 에이전트가 틀렸을 때 그것이 드러나고 멈추는 구조다. 다음 작업은 채점 시트에 남은 마지막 1점, 기계 게시물의 아이덴티티를 정리하는 것이다. 담당자 지정을 수명주기 규칙으로 명문화하고, [OPEN] 질문 댓글까지 고정 마커를 통일한다.