
AI 에이전트에게 구현을 맡기기 위한 하네스(Harness) ─ 규칙 레지스트리와 hooks로 제약하기
요약
AI 에이전트가 대량의 코드를 작성할 때 발생할 수 있는 실수를 방지하기 위해, 기계가 읽을 수 있는 형태의 제약 조건인 '하네스(Harness)'를 구축하는 방법을 소개합니다. 규칙 레지스트리와 hooks를 통해 AI의 생성물을 실행 전후 및 CI 단계에서 검증하는 설계 방식을 다룹니다.
핵심 포인트
- AI 에이전트의 고속 코드 생성에 대응하기 위한 기계적 검증 체계 필요
- 규칙 레지스트리를 통해 제약 조건을 코드화하고 일괄 실행
- 문서(CLAUDE.md)와 실제 구현 간의 불일치(Drift) 방지 전략
- 정규 표현식 등을 활용한 특정 라이브러리 사용 금지 등 구체적 규칙 적용
TenkaCloud라는, 실제 AWS 계정 위에서 클라우드 경연을 개최하는 OSS를 만들고 있습니다(susumutomita/TenkaCloud, Apache-2.0). 프로덕트 자체에 대한 설명은 온보딩 가이드 기사를 참조해 주세요. 구현의 대부분은 Claude Code와 같은 AI 에이전트에게 작성하게 하고 있습니다.
사람이 작성하는 코드라면, 리뷰를 통해 알아챌 수 있다는 전제에 의지할 수 있습니다. AI가 대량으로 작성하는 코드는 그 전제가 무너집니다. 똑같은 실수를 고속으로 반복할 수 있기 때문입니다. 그래서 TenkaCloud에서는 "지켜줬으면 하는 규칙"을 문서 기술만으로 끝내지 않고, 기계가 읽을 수 있는 형태로 코드화하고 있습니다.
여기서 말하는 하네스(Harness)란, AI 에이전트의 조작이나 생성물을 실행 전·실행 후·커밋 전·CI 단계에서 검사하는 실행 가능한 제약군입니다. 규칙 레지스트리(Rule Registry)는 해당 제약을 동일한 인터페이스로 등록하고 일괄 실행하는 메커니즘입니다. 이 기사에서는 TenkaCloud의 규칙 레지스트리와 hooks 설계에 대해 소개합니다. 아울러, 하네스 자체도 사람이 만드는 이상 드리프트(Drift)한다는 실제 사례를 작성합니다.
make harness
규칙을 코드로 만들기: TenkaCloud는 CLAUDE.md에 "지켜줬으면 하는 규칙"을 쓰는 것에 그치지 않고, 기계 체크가 가능한 것은 코드로 만들고 있습니다. 구현은 .claude/harness/src/rules/에 규칙 하나당 파일 하나로 구성됩니다. make harness를 실행하면 .claude/harness/bin/architecture.ts가 동작합니다. 스테이징된 파일에 대해 이 규칙 레지스트리를 전부 실행하여, 위반 사항을 에러로 보고합니다.
// .claude/harness/src/rules/index.ts
export const architectureRules = [
adrMustBeHtml,
...
규칙의 내용은 예를 들어 "Secrets Manager의 import를 금지한다"는 것입니다. TenkaCloud에는 상설 운영 비용을 억제하는 방침이 있습니다. 기밀값은 Secrets Manager가 아니라, SSM Parameter Store의 Standard tier의 SecureString에 두기로 결정했습니다. Standard tier는 Parameter Store 자체의 추가 요금이 없습니다. 단, SecureString에서 사용하는 AWS KMS의 요금은 별도입니다. 이를 각 행의 정규 표현식 스캔을 통해 기계적으로 검출합니다.
// .claude/harness/src/rules/secrets-manager-forbidden.ts
const SECRETS_MANAGER_IMPORT_RE =
/(?:from\s+|import\s*\(?\s*|require\s*\(\s*)["'](@aws-sdk\/client-secrets-manager|aws-cdk-lib\/aws-secretsmanager)["']/;
...
이 파일의 주석에는 솔직한 경위가 적혀 있습니다.
CLAUDE.md / harness.md는 본 규칙을 기계 체크 대상으로 기재하고 있었으나, 구현이 존재하지 않았음 (= 거짓된 안전 보장). 문서의 계약에 구현을 맞춤.
즉, "문서에는 기계 체크를 한다고 적혀 있는데 구현이 없는" 상태가 과거에 한 번 있었다는 뜻입니다. 문서의 기술과 구현은 방치하면 쉽게 어긋납니다. 그 외에도 다음과 같은 규칙들을 하나씩 늘려가고 있습니다.
handler-must-not-call-fetch : 핸들러에서 직접 fetch를 호출하지 않도록 함
adr-self-contained : ADR에 채팅 문맥이나 AI 에이전트의 역할 분담 메모를 남기지 않음
iam-wildcard-needs-justify : IAM의 와일드카드 사용 시 근거 주석 필수
Claude Code 자체를 hooks로 제약하기
규칙 레지스트리는 커밋 전에 make harness를 명시적으로 실행하면 효과가 있습니다. 하지만 AI 에이전트가 매번 그것을 기억하고 실행할 것이라는 보장은 없습니다. 그래서 TenkaCloud는 Claude Code의 hooks 메커니즘(.claude/settings.json)을 사용하고 있습니다.
실행 전에 판정할 수 있는 것은 PreToolUse에서 거부합니다. 편집 후에야 판정할 수 있는 것은 PostToolUse에서 위반을 반환하여 수정을 요구합니다.
설정 파일에 대한 직접 편집은 PreToolUse hook에서 기계적으로 차단합니다.
# .claude/hooks/guard-config.sh (발췌)
case "$BASENAME" in
.eslintrc*|eslint.config.*|biome.json|.prettierrc*|prettier.config.*)
...
lint나 테스트 에러에 막힌 AI 에이전트가 코드를 수정하는 대신 설정을 완화하여 "해결"해 버리는 것은 흔히 발생하는 실패 사례입니다. PreToolUse의 exit 2는 툴 호출(Tool Use) 자체를 거부합니다. 따라서 설정 파일은 변경되지 않습니다. .env 계열 파일에 대한 직접 편집도 같은 이유로 금지하고 있습니다.
편집 직후에는 PostToolUse hook을 통해 스텁(stub)이나 폴백(fallback) 코드를 감지합니다.
# .claude/hooks/quality-guard.sh (발췌)
if echo "$CONTENT" | grep -Eqi 'fallback to empty|empty dataset|empty values|stub problem|returning empty'; then
echo "BLOCKED: 임시방편인 fallback / stub이 감지되었습니다: $FILE_PATH" >&2
...
이 부분은 PreToolUse와 강제력이 다릅니다. 로그상에는 BLOCKED라고 표시되지만, PostToolUse는 툴이 성공한 후에 실행됩니다. 따라서 이미 작성된 파일을 이전 상태로 되돌리지는 않습니다. exit 2를 통해 표준 에러(stderr)를 Claude Code에 위반 사항으로 반환하고, 다음 응답에서 수정을 유도합니다. Claude Code의 공식 레퍼런스에서도 툴 호출 자체를 방지하려면 PreToolUse를 사용하라고 설명되어 있습니다.
동일한 PostToolUse hook은 UI 레이어에서 직접 fetch(를 호출하는 코드도 감지합니다. process.env.API_URL을 직접 읽는 코드도 대상입니다. 감지된 경우 Claude Code에 수정을 요구합니다.
단, 현재 matcher는 Write|Edit입니다. Bash를 통한 파일 쓰기 작업은 이 hook을 거치지 않습니다. hooks에만 완전성을 기대할 수 없는 이유 중 하나입니다.
나아가, git commit을 포함한 Bash 명령어를 실행할 때는 PreToolUse hook에서 make before-commit을 먼저 실행합니다. 이 명령어에서는 lint와 테스트를 실행합니다. 실제 커밋 시에는 Husky의 .husky/pre-commit도 독립적으로 동작합니다. Husky는 make before-commit과 품질 게이트(quality gate)를 실행합니다. 로컬 hooks의 외부에서는 CI가 typecheck, coverage, build 등을 재검증합니다.
솔직한 포인트: hook 자체도 드리프트(drift)한다
지금까지의 메커니즘은 강력해 보입니다. 하지만 이 글을 쓰기 위해 설정을 다시 읽어보다가 실제로 드리프트를 발견했습니다. git commit 전 후크(hook)의 구현이 다음과 같이 되어 있었습니다.
"command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -q 'git commit'; then ROOT=$(git rev-parse --show-toplevel); bun \"$ROOT/scripts/ai-improvement-loop.ts\" --staged --fail-on=high --root=\"$ROOT\" && make -C \"$ROOT\" before-commit; fi"
scripts/ai-improvement-loop.ts는 리포지토리의 대규모 리팩토링(Trunk migration, #440) 과정에서 이미 삭제되었습니다. &&로 연결되어 있기 때문에, 존재하지 않는 스크립트 실행에 실패하게 됩니다. 그 결과, 후속 단계인 make before-commit은 한 번도 실행되지 않습니다.
일반적인 git commit의 경우, 실제 Git 후크(.husky/pre-commit)가 make before-commit을 독립적으로 실행합니다. 따라서 커밋 시의 품질 게이트 자체는 남아 있었습니다. 하지만 Claude Code가 git commit
실행하기 직전의 피드백 계층은 조용히 죽어 있었습니다.
게다가, .husky/pre-commit에는 다음과 같은 주석이 남아 있습니다.
# 구 architecture-harness / ai-improvement-loop 는 ProtoShip 이관으로 제거됨.
# 이관 완료 후에 새로운 harness를 재도입함 (Phase 2 이후).
여기서 말하는 구(old) harness는 삭제된 architecture-harness와 ai-improvement-loop를 가리킵니다. 반면, 현재 리포지토리에는 이미 .claude/harness/와 make harness가 존재합니다. "새로운 harness는 향후 재도입한다"라는 설명 주석도 현 상황과는 어긋나 있었습니다.
발견한 당일에, 죽어 있는 호출을 제거하는 수정 PR(TenkaCloud #2730)을 제출했습니다. 수정 후에는 make -C "$ROOT" before-commit만 실행합니다.
앞 절의 secrets-manager-forbidden과 함께 보면, 동일한 종류의 실패가 반복되고 있습니다.
- 체크한다고 문서에는 적혀 있지만, 규칙(rule)이 없음
- hook은 설정되어 있지만, 호출 대상이 없음
- 주석이 현재 구성을 설명하지 못함
Harness는 한 번 구축하고 끝나는 것이 아닙니다. 리팩터링(Refactoring)을 할 때마다 유지보수하지 않으면, 체크하고 있다고 생각하면서 실제로는 아무것도 체크하지 않는 기간이 발생합니다.
Harness 자체도 테스트 대상으로 삼기
TenkaCloud에는 make harness-test가 있습니다. 각 규칙에는 정상 사례와 위반 사례에 대한 유닛 테스트(Unit Test)를 포함하고 있습니다. 예를 들어 secrets-manager-forbidden은 Secrets Manager의 import를 감지합니다. 반면, SSM의 import는 통과하는지도 테스트합니다.
하지만 규칙 단독의 테스트가 통과한다고 해서, .claude/settings.json으로부터 해당 규칙이나 스크립트로 도달할 수 있다는 보장은 없습니다. 이번에 고장 난 것은 규칙의 판정 로직이 아니었습니다. 설정과 구현을 잇는 배선이었습니다. 다음에 필요한 것은 다음과 같은 harness의 생사 확인(Health Check)입니다.
.claude/settings.json이 참조하는 스크립트의 존재와 실행 권한을 검사한다- 샘플
TOOL_INPUT을 전달하여, 각 hook의 종료 코드(Exit Code)와 메시지를 스모크 테스트(Smoke Test)한다 make harness-test에 더해, PR 차분(diff)을 입력으로 하여 동일한 규칙을 CI에서도 재실행한다
마치며
AI 에이전트에게 구현을 맡기는 데 있어, TenkaCloud가 하고 있는 일을 나열하면 다음과 같습니다.
- 규칙을 문서로 끝내지 않고,
.claude/harness/에 코드와 유닛 테스트로 보유한다 - PreToolUse를 통해, 설정 파일의 변경 등 실행 전에 거부할 수 있는 조작을 차단한다
- PostToolUse를 통해, 편집 후에 판정되는 위반 사항을 즉시 반환하여 수정을 요구한다
- Husky와 CI를 통해, 에이전트의 hooks와는 독립된 품질 게이트(Quality Gate)를 갖는다
강제성의 강도에는 단계가 있습니다. PreToolUse는 도구 호출을 중단할 수 있지만, PostToolUse는 이미 수행된 편집을 취소할 수 없습니다. 규칙 레지스트리(Rule Registry)는 규칙의 구현이 존재하더라도 설정에서 호출되지 않으면 작동하지 않습니다. Husky도 로컬에 존재하는 한 CI와는 역할이 다릅니다.
결국, hooks와 harness는 "만들면 끝"이 아닙니다. 프로덕트 코드와 마찬가지로 테스트하고, 고장 나지 않았는지 지속적으로 확인해야 하는 대상입니다. AI 에이전트를 검사하는 harness에도, harness 자체가 살아있는지를 검사하는 메커니즘이 필요했습니다. 이 글을 쓰는 과정에서, 그 필요성을 자신의 리포지토리에서 증명하게 되었습니다.
Discussion

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