📚 엔지니어 · 세션 · 에이전틱 개발 (수정중) · L04

Hooks (수정중)

작성일자 2026-06-21  ·  수정일자 2026-06-21  ·  강의차수 L04  ·  예상소요 30분

목표 이 강의가 끝나면 학습자가 hook 이 settings.json 의 PreToolUse 로 도구 실행 직전에 끼어들어 exit 2 로 커밋을 차단하는 원리를 설명하고, popax 의 hook 3종이 각각 무슨 규율을 강제하는지, 그리고 CLAUDE.md 규칙이 언제 hook 으로 "승격" 되는지 판단할 수 있다.

1. 부탁과 강제

공사장 입구를 떠올려 보자. "안전모 꼭 쓰세요" 라고 안내문만 붙여두면 대개는 쓴다. 하지만 급하면 깜빡하는 사람이 나온다. 반면 문 앞에 사람이 서서 안전모 없으면 아예 못 들어가게 막으면, 깜빡할 수가 없다. hook(어떤 작업 직전에 자동으로 끼어들어 검사하는 스크립트)은 이 문 앞의 사람 같은 것이다.

CLAUDE.md 에 "버그를 고치면 docs/issues/ 문서를 같은 커밋에 넣어라" 라고 적어두면, Claude 는 대개 지킨다. 하지만 컨텍스트(그동안 주고받은 대화·맥락)가 길어지거나 급하면 깜빡한다. 규칙은 "지켜주길 바라는 것" 이라 100% 가 아니다.

hook 은 이걸 강제로 바꾼다. 버그 키워드가 든 커밋인데 이슈 문서가 없으면, 커밋 자체가 안 된다. Claude 의 기억이나 선의에 기대지 않고, harness(Claude Code 를 돌리는 실행 틀)가 도구 실행을 가로막는다.

핵심 한 줄: CLAUDE.md = 부탁(soft), hook = 강제(hard). 규칙으로 충분하면 CLAUDE.md, "절대 빠지면 안 되는" 것이면 hook 으로 박는다.


2. PreToolUse 메커니즘

Bash 도구실행 직전hook명령 검사exit 0통과 → 도구 실행exit 2차단 → stderr 안내stdin JSON

popax 의 hook 은 전부 PreToolUse 타입이다. 이름 그대로 "도구를 쓰기(Tool Use) 전(Pre)" 이라는 뜻이다. Claude 가 어떤 도구(여기선 Bash, 명령어를 실행하는 도구)를 실행하기 직전에 harness 가 hook 스크립트를 먼저 돌린다.

hook 과 harness 가 주고받는 약속은 단순하다:

  • hook 은 stdin(프로그램에 들어오는 입력 통로)으로 JSON(값을 이름표와 함께 담는 데이터 형식) 을 받는다. 그 안에 Claude 가 실행하려는 명령이 들어 있다 (.tool_input.command).
  • hook 이 exit 0 으로 끝나면(프로그램이 끝날 때 남기는 숫자 0 은 "이상 없음" 을 뜻한다) → 통과, 도구가 그대로 실행된다.
  • hook 이 exit 2 로 끝나면 → 차단. 도구가 실행되지 않고, hook 이 stderr(오류 메시지를 내보내는 통로) 로 출력한 메시지가 Claude 에게 전달돼 "왜 막혔는지" 를 알려준다.

popax 의 세 hook 은 모두 이 stdin 에서 명령을 꺼내 git commit 인지 보고, 맞으면 규칙을 검사해 위반이면 exit 2 한다.

INPUT=$(cat)                                              # stdin JSON
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
echo "$COMMAND" | grep -q "git commit" || exit 0          # 커밋 아니면 그냥 통과
# ...규칙 검사... 위반이면:  echo "..." >&2;  exit 2

3. settings.json 등록

hook 을 "언제 돌릴지" 는 .claude/settings.json 이 정한다. popax 의 설정 전체는 이렇게 짧다:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/check-bugfix-issue-doc.sh" },
          { "type": "command", "command": ".claude/hooks/update-skill-agent-list.sh" },
          { "type": "command", "command": ".claude/hooks/check-except-logger-exception.sh" }
        ]
      }
    ]
  }
}

읽는 법: Bash 도구를 쓰려 할 때마다(matcher 는 "어떤 도구일 때 hook 을 돌릴지" 고르는 조건이다), 등록된 3개 스크립트가 차례로 돈다. 각 스크립트가 "이 명령이 내 관심사(git commit)인가" 를 스스로 판단하므로, matcher 는 넓게 Bash 로 두고 걸러내는 일은 스크립트가 한다. 이 파일도 .claude/ 안에 있으니 git 에 커밋돼 팀 전원에게 같은 강제가 적용된다 (스킬과 같은 공유 원리).

세 hook 이 각각 강제하는 규율은 다음과 같다(상세는 §4~6).

hook 강제하는 규율
check-bugfix-issue-doc.sh 버그 수정 커밋에 이슈 문서 포함
update-skill-agent-list.sh 스킬·에이전트 변경 시 목록 동기화
check-except-logger-exception.sh except 블록의 logger.exception 사용 (AST)

4. Hook ① 문서 규율 강제

규칙(CLAUDE.md): "비자명한(한눈에 뻔하지 않은) 버그를 고치면 docs/issues/{번호}_{slug}.md 를 같은 커밋에 포함한다." 이 hook(check-bugfix-issue-doc.sh)이 그걸 강제한다.

  1. 커밋 메시지에 버그 키워드(수정·fix·버그·bug·크래시·오류·에러)가 있는지 본다.
  2. 있으면, staged 파일(이번 커밋에 담으려고 골라둔 파일)에 docs/issues/ 가 포함됐는지 본다.
  3. 없으면 → exit 2 로 차단 + 안내:
⚠️ 버그 수정 커밋에 이슈 문서가 포함되지 않았습니다.
docs/issues/{번호}_{slug}.md 파일을 작성하여 같은 커밋에 포함해주세요.
(단순 오타나 자명한 수정은 무시해도 됩니다)

여기서 "버그 키워드를 메시지에서만" 찾는 게 중요하다. 명령 전체를 훑으면 git add 버그수정.py 의 파일명에 든 글자까지 걸려 엉뚱하게 막힌다(오탐, §7).


5. Hook ② 목록 일관성 강제

L11 §8 에서 "스킬·에이전트 목록을 자동 생성하는 스킬(/update-skill-agent-list)" 을 봤다. 문제: 사람이 스킬을 바꾸고 그 목록 갱신을 깜빡하면 목록 문서가 실제와 어긋난다. 이 hook(update-skill-agent-list.sh)이 그 깜빡함을 막는다.

  • staged 에 .claude/skills/ 또는 .claude/agents/ 변경이 있는데,
  • docs/skill_agent_list/ 가 같이 staged 되지 않았으면 → exit 2:
⚠️ skill 또는 agent가 변경되었지만 docs/skill_agent_list/가 업데이트되지 않았습니다.
/update-skill-agent-list 를 먼저 실행한 후 커밋에 포함해주세요.

즉 "스킬을 고쳤으면 목록도 같은 커밋에" 를 강제한다. L11(스킬)·L12(에이전트)·L13(hook)·자동 목록(스킬)이 여기서 한 고리로 물린다. .claude/ 는 서로를 강제하는 한 시스템이라는 게 이 hook 에서 가장 또렷하다.


6. Hook ③ 코드 품질 강제 (AST)

가장 정교한 hook. 출처가 실제 사고다. popax 전사/회의록 task 에서 except(오류를 잡아 처리하는 블록) 가 logger.error() 만 써서 traceback(오류가 어디서 어떻게 났는지 보여주는 호출 자취)이 통째로 사라져 디버깅이 막혔던 사건이다.

규칙: except 블록 안에서는 logger.error/.warning 이 아니라 logger.exception 을 써야 traceback 이 남는다.

  • 이 hook(check-except-logger-exception.sh)은 staged 된 .py 파일을 모아 check_except_logger.py (파이썬 AST 검사기)에 넘긴다. AST 는 코드를 글자 그대로가 아니라 문법 구조로 뜯어 본 것이다.
  • 그 AST 로 except 핸들러 안쪽의 logger.error/.warning 호출만 정확히 집어낸다 (글자만 맞춰보는 게 아니라 구문을 분석해서).
  • 그중 이번 커밋에서 새로 추가된(+) 라인에 걸린 것만 차단한다. 기존 코드의 위반까지 막으면 막히는 게 너무 많기 때문이다.
# check_except_logger.py: staged diff 의 추가 라인만 위반으로 (발췌)
def added_line_numbers(repo, path):
    diff = _git(repo, 'diff', '--cached', '-U0', '--', path)
    # @@ ... +N @@ 헤더를 따라 추가된 + 라인 번호만 모은다

이건 단순 grep(글자만 찾는 검색)으로는 못 한다. except 밖의 logger.error(오류가 아닌 정상적인 실패 상황을 남기는 로그)는 건드리면 안 되기 때문이다. 그래서 AST 까지 동원한다.


7. 좋은 hook 의 디테일

hook 은 "막는" 도구라, 막지 말아야 할 걸 막으면(오탐) 일이 멈추고 막아야 할 걸 놓치면(미탐) 무용지물이다. popax hook 들의 주석에는 이 함정을 피한 흔적이 백로그 번호(#D-...)와 함께 남아 있다:

  • 메시지만 검사: 키워드를 커밋 메시지에서만 찾는다. 명령 전체를 훑으면 git add 버그.py 파일명 때문에 엉뚱하게 막힌다(오탐, #D-260529001915).
  • cd <dir> 를 따라간다: worktree(같은 저장소를 다른 폴더에 따로 펼쳐 작업하는 방식)·src/ 하위 구조에선 커밋이 다른 디렉토리에서 일어난다. 그래서 명령에 든 cd 를 읽어내 그 위치의 repo 에서 staged 를 본다 (^docs/issues/ 처럼 경로 첫머리를 고정해 찾지 않는 이유: worktree 에선 실제 경로가 src/docs/issues/... 라 첫머리 고정이 빗나가기 때문).
  • 추가된 라인만: 기존 코드 위반까지 막으면 커밋이 통째로 막혀 못 쓴다 → 새 + 라인만.
  • 보수적 차단: 메시지를 못 읽는 경우(-F 파일로 메시지를 넘기는 등)엔 차단하지 않는다. "확실할 때만 막는다" 가 원칙이다. 애매하면 통과시켜 일을 안 멈추게 한다.

교훈: hook 은 "규칙이 맞나" 보다 "오탐 없이 정확히 그 경우만 막나" 가 어렵다. 그래서 자잘한 디테일과 실제 사건 기반 보정이 쌓인다.


8. hook 승격 기준

세 단계로 정리된다:

  1. CLAUDE.md 규칙(말): "이렇게 해주세요". 대부분은 이걸로 충분하다.
  2. 반복 위반 → 교훈: 같은 실수가 반복되면 이슈/교훈으로 기록된다.
  3. hook 으로 승격(강제): "말로는 계속 빠진다" 가 확인된 규칙만 hook 으로 박는다.

popax 의 3개 hook 이 모두 이 길을 밟았다. 이슈 문서 누락, 목록 불일치, traceback 유실 모두 실제로 반복됐기에 강제로 올라갔다. 같은 패턴이 agentkim 에도 있다: .claude/hooks/block_direct_meeting_minutes.sh 가 회의록을 곧바로 만드는 걸 막아, 반드시 /meeting-minutes 스킬(참석자·날짜를 먼저 확인하는 관문)을 거치게 강제한다.

뒤집어 말하면: 모든 규칙을 hook 으로 만들 필요는 없다. 강제에는 비용(잘못 막을 위험·유지보수)이 따르니, "빠지면 사고 나고 + 말로는 자꾸 빠지는" 것만 골라 승격한다.


마무리

  • 정리: hook 은 settings.json 의 PreToolUse 로 도구 실행 직전에 끼어들어, stdin 의 명령을 보고 규칙 위반이면 exit 2 로 차단하는 강제 장치다(exit 0 통과). popax 는 Bash matcher 에 3개를 걸어 git commit 을 검사한다. 버그 커밋의 이슈 문서 누락, skill/agent 목록 불일치, except 의 traceback 유실(AST·추가 라인만)을 잡는다. 좋은 hook 은 오탐·미탐을 줄이는 보수적 디테일(메시지만·cd 추적·추가 라인만·애매하면 통과)이 핵심이다. CLAUDE.md(부탁)로 자꾸 빠지는 규칙만 hook(강제)으로 승격한다.
  • 흔한 함정: hook 을 너무 빡빡하게 짜서 정상 커밋까지 막는 것(오탐). 그러면 팀이 --no-verify 로 hook 을 건너뛰기 시작해 hook 전체가 무력화된다. "확실할 때만 막는다" 가 생명.
  • 다음 시간: M3 의 나머지(에이전틱 엔지니어링 마인드셋·Claude Code 기본기·CLAUDE.md & Memory)로 채워간다. .claude 3부작(스킬·에이전트·hook)은 여기서 한 바퀴 닫혔다.
  • 자습 권장: popax/src/.claude/hooks/check-bugfix-issue-doc.sh 를 한 줄씩 읽어 보기. 짧고, 오탐 방지 주석이 잘 달려 있다. 본인 프로젝트에서 "말로는 자꾸 빠지는" 규칙 하나를 떠올려, 그게 hook 승격 후보인지(빠지면 사고가 나는가?) 가늠해 보기.
이 강의를 학습하셨나요?