Claude Code의 Hook을 '누가 검사할 것인가' 문제: 관점, 테스트, 변이 테스트로 품질 키우기
요약
본 글은 Claude Code의 Hook을 활용하여 TODO.md 형식 체크 시스템을 구축한 과정을 다룹니다. 단순히 코드를 검사하는 것을 넘어, '검사하는 쪽(Hook)' 자체의 품질과 정확성을 보장하기 위해 4개의 계층 구조를 설계했습니다. 특히, 테스트 케이스가 놓치기 쉬운 '관점(Viewpoint)'을 YAML 파일로 외부화하여 사람이 직접 판단하고 시스템에 반영할 수 있도록 개선한 점이 핵심입니다.
핵심 포인트
- Hook의 신뢰성 확보를 위해 검사하는 쪽 자체를 검사하는 구조 설계
- TODO.md 파서 버그 사례를 통해 '건수'만 보는 테스트의 한계점 발견
- 테스트 케이스(Viewpoint)를 YAML로 외부화하여 사람이 판단할 수 있게 함
서론
Claude Code의 Hook으로 'TODO.md 형식 체크'를 만들었습니다. 작동은 했지만, 사용하다 보니 '이 체크가 정말 올바르게 검사하고 있는 걸까?' 하는 의문이 들기 시작했습니다.
Hook은 'Claude의 작업을 검사하는 쪽'입니다. 만약 이 검사하는 쪽 자체가 잘못되었다면, 아무도 알아차릴 수 없습니다. 이 글은 거기서 검사하는 쪽을 검사하는 시스템을 만든 기록입니다.
- 예상 독자: Claude Code의 Hook을 작성하기 시작한 사람, AI가 작성한 코드의 품질에 불안감을 느끼는 사람
- 소재: TODO.md 형식(formatting)을 검사하는 Hook 그룹 (Python 제작)
- 환경: Windows / PowerShell / Python / pytest / PyYAML
저는 하나의 프로젝트에서만 시도했습니다. '이렇게 하면 잘 될 것이다'가 아니라, '이렇게 해봤고, 이렇게 망가졌고, 이렇게 고쳤다'는 기록으로 읽어주시면 감사하겠습니다.
소재: TODO.md 형식 체크 Hook
TODO.md에는 다음과 같은 형식 규칙을 정했습니다.
- 1 작업당 1 줄
- 착수 전에 결정할 것은
- **착수 전에 결정할 것**: 본문
의 형태로 작성- 리스트 기호는-`
만 사용-
들여쓰기는 공백만 사용-
완료된 작업은 삭제
이것을 지키게 하기 위해, Hook을 4개 등록했습니다.
| 이벤트 | 역할 |
|---|---|
| SessionStart | TODO의 요약과 설계서 개요를 Claude에게 전달하고, 형식 위반 시 경고합니다 |
| ... |
시스템 전체 구조
품질을 보장하기 위해, 4개의 계층(layer)을 만들었습니다.
① 규칙 (사람이 작성)
↓ 이것을 지키게 하고 싶다
② 검사 코드 (Hook) ←── 테스트로 확인 ── ③ 테스트 (pytest)
...
- 규칙: 사람이 결정합니다. 'TODO.md는 이렇게 작성한다' -
- 검사 코드: 규칙을 기계적으로 검사하는 Python (
todo_lint.py등) - - 테스트: 검사 코드가 올바르게 작동함을 확인하는 pytest -
- 관점(Viewpoint): '무엇을 시도해야 하는가'를 YAML로 나열한 것
왜 4개 계층인가
'검사하는 쪽을, 누가 검사할 것인가'라는 문제가 있기 때문입니다.
| 검사되는 쪽 | 검사하는 쪽 |
|---|---|
| 수정된 파일 | Hook |
| ... | |
| 마지막 줄이 중요합니다. '관점이 충분한가'는 AI가 판단할 수 없습니다. AI는 작성된 관점에 대해서 테스트를 작성할 수는 있지만, '애초에 쓰여지지 않은 관점(viewpoint)이 있다'는 사실은 스스로 깨닫지 못합니다. |
그래서 관점을 YAML이라는 형태로 외부로 빼내어 사람이 보고 판단할 수 있게 했습니다. 그리고 관점과 테스트의 불일치는 기계적으로 감지합니다.
실패 1: 2줄 작성으로 건수는 같지만 내용이 망가짐
처음 파서(parser)에는 이런 버그가 있었습니다.
착수 전에 결정할 것을 2줄에 걸쳐 작성하면, 2번째 줄이 다른 작업의 본문에 붙어버립니다. 게다가 작업 건수는 변하지 않기 때문에, '건수가 일치하는지'만 보는 테스트에서는 통과해 버립니다.
원인은 3개의 계층에 있었습니다.
- 사람이 읽는 문서를 그대로 기계의 입력으로 사용했다.
- 파서가 '연속 줄(continuation line)'을 예상하지 못했다.
- 테스트가 건수만 보고 있었다.
교훈은 '건수만 보는 테스트는, 내용이 망가지는 것을 놓친다' 입니다.
관점을 YAML로 나열하기
그래서 테스트 전에 '무엇을 시도할지'를 YAML에 작성하기로 했습니다.
source: .claude/hooks/todo_lint.py
tests: tests/hooks/test_todo_lint.py
viewpoints:
...
관점은 **축(axis)**으로 정리했습니다.
| 축 | 예시 |
|---|---|
| 입력 작성 방식 | 전각 콜론, 탭 들여쓰기, *나 + 기호 |
| ... | |
| 여기서 배운 것은, **'관점을 하나 추가한다'가 아니라 '축을 추가한다'**고 생각하면 빠뜨리는 것이 적다는 것입니다. |
YAML의 함정
설명:안에:(콜론과 공백)이 있으면 구문 오류가 발생한다 → 블록 스칼라>를 사용 -
no나on을 나체로 쓰면 부울 값(boolean value)이 된다
3가지 대조표
관점(観点)과 테스트, 검사 코드 사이에서 3가지의 대조를 수행합니다.
- 관점 ↔ 테스트: 관점의
id에 대응하는 테스트가 있는지 -
검사 코드 ↔ 관점: 검사 코드에 있는 규칙 id(ast로 추출)가 관점에 기재되어 있는지 -
미지의 규칙명은 예외:lint(rules=...)에 존재하지 않는 규칙명을 전달하면ValueError
두 번째 이유 때문에, 관점의 규칙: 항목에는 다음 작성법을 준비했습니다.
| 작성법 | 의미 |
|---|---|
규칙: bad_checkbox | 해당 규칙이 발생하는 것을 확인하는 관점 |
규칙: [a, b] | 여러 개의 규칙 |
규칙: なし | 규칙 id를 갖지 않는 분기의 관점 |
규칙: {출발하지 않음을 확인: [bad_checkbox]} | 오탐(그 규칙이 발생하지 않음)을 확인하는 관점 |
규칙: 출현하지 않음을 확인: [...]와 작성하면 YAML로 읽히지 않아, 1줄 매핑으로 했습니다.
실패 2: 변이 테스트에서 발견된 허점
'테스트가 통과한다'는 것과 '테스트가 검사 코드를 지키고 있다'는 것은 별개입니다.
그래서 **변이 테스트(Mutation Testing)**를 했습니다. 검사 코드를 일부러 망가뜨려서, 테스트가 떨어지는지 확인합니다. 떨어진다면, 그 테스트는 지키고 있지 못한 것입니다.
발견된 허점의 예:
-
PreToolUse의 대상 툴
TARGET_TOOLS에서Write를 제외해도 테스트가 통과함 - 테스트가Edit으로만 시도했음 - 라인 커버리지는 100%였음 (실행은 되었지만, 검증하지는 않음) -
테스트가
라인 커버리지로는 '실행되지 않은 라인'밖에 찾을 수 없습니다. '실행되었지만 아무것도 확인하지 못한 것'은 변이 테스트가 아니면 발견할 수 없습니다.
게다가, 이 허점은 관점에도 테스트에도 기재되어 있지 않았기 때문에, 관점 ↔ 테스트의 대조에서도 발견되지 않습니다. 그래서 '검사 코드 ↔ 관점'의 대조를 추가했습니다.
실패 3: 이벤트별로 반환값이 다름
Hook은 이벤트에 따라 사용할 수 있는 반환값이 다릅니다.
| 이벤트 | 할 수 있는 것 |
|---|---|
| SessionStart | 표준 출력을 Claude에게 전달하는 것만 가능. 막을 수 없음 |
| PreToolUse | permissionDecision으로 ask / deny / allow |
| PostToolUse | `decision: |
cd <프로젝트 폴더>
python -m pytest tests -q # 건수와 실패를 확인
git log --oneline # 커밋이 실재하는지
...
'Claude의 말'이 아니라 '명령어 출력'을 근거로 삼는 운영 방식입니다.
한계점
| 범위 | 상태 |
|---|---|
| 관점 ↔ 테스트, 검사 코드 ↔ 관점 대조 | Stop Hook에서 자동 |
| Hook을 편집한 후의 pytest 실행 | 수동 (자동화는 TODO) |
| 관점이 충분한지 | 사람이 판단 |
| Bash를 경유하는 쓰기 작업 | PreToolUse / PostToolUse를 우회함 |
| settings.json의 절대 경로 | 기계 고유. 형식 검사는 미작성 |
stop_hook_active에 의한 2차 제동 | |
| 실제 세션에서의 확인이 미실시 |
배운 점
- Hook은 '검사하는 쪽'입니다.
검사하는 쪽에게도 검사가 필요합니다. - '테스트가 통과한다'보다 '고장 냈을 때 테스트가 실패하는지'를 확인합니다 (변이 테스트). - 건수만 보는 테스트는 내용의 손상을 놓칠 수 있습니다. - '무엇을 시도할지'를 YAML로 내보내면, 사람이 부족한 점을 판단할 수 있습니다.
- 기계로 검사할 수 있는 것은 Hook에, 판단이 필요한 것은 사람에게. AI의 보고는 직접 실행해서 확인해야 합니다.
마지막으로
이 시스템은 하나의 프로젝트에서만 검증했습니다. 두 번째 프로젝트에서 사용할 때, 범용화할 부분(관점 대조)과 고유한 부분(TODO.md 전용 검사)을 분리할 계획입니다.
테스트는 현재 242건이며, 모두 통과하고 있습니다. 다만, 건수가 늘어나는 것 자체가 품질은 아닙니다. '고장냈을 때 떨어지는지'를 계속해서 확인해 나갈 것입니다.
Discussion

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