훅(Hooks)으로 동작 강제하기

Hooks는 Claude Code 라이프사이클의 특정 시점에 자동으로 실행되는 처리입니다. CLAUDE.md의 '부탁'과 달리 모델 판단과 무관하게 항상 동작하는 '규칙'을 만드는 방법과 대표 이벤트 10종, 설정 위치, 예시 2가지를 정리합니다.

12분 갱신

우리가 교육에서 반복하는 문장이 있습니다. "프롬프트는 부탁, 하네스는 강제." CLAUDE.md에 "커밋 전에 반드시 테스트해라"라고 적어도 Claude는 그것을 맥락으로 읽을 뿐, 백 번 중 몇 번은 건너뜁니다. 반면 훅은 Claude가 무엇을 결정했든 정해진 시점에 셸 명령이 돌아갑니다. 실수를 규칙으로 바꾸는 장치가 바로 훅입니다.

이 글은 앞 글에서 본 settings.json의 hooks 키를 깊게 다룹니다. 언제 훅이 필요한지, 대표 이벤트 10종, 설정 위치, 위험 명령 차단과 자동 포맷 예시, 설계 원칙과 디버깅 순서를 봅니다. 다음 글은 「슬래시 명령어와 Skills」입니다.

핵심 요약

  • 훅은 라이프사이클 이벤트(도구 호출 전후, 세션 시작, 응답 종료 등)에 걸어 두는 명령입니다. 모델 판단과 무관하게 항상 실행됩니다.
  • PreToolUse에서 종료 코드 2를 반환하면 도구 호출이 막힙니다. allow 규칙보다도 우선합니다.
  • 설정은 settings.json 네 층 중 어디든, 또는 Skill·에이전트 프론트매터에 둡니다.
  • 훅 스크립트는 stdin으로 JSON을 받고, stdout에 JSON을 돌려주거나 종료 코드로 결과를 알립니다.
  • 성공은 조용히, 실패만 시끄럽게. 훅 출력은 매우 길면 잘릴 수 있습니다.

언제 훅이 필요한가

세 가지 신호가 있습니다.

  1. 같은 실수가 두 번 이상 반복될 때. 우리 팀은 "에이전트가 반복하는 실수는 발견 즉시 CLAUDE.md 또는 훅으로 올린다"를 원칙으로 둡니다. 두 번째부터는 훅입니다.
  2. 실행 시점이 정해져 있을 때. "파일을 고칠 때마다 포맷", "세션이 시작될 때 브랜치 확인"처럼 "언제"가 명확하면 CLAUDE.md보다 훅이 맞습니다.
  3. 돈이나 데이터가 걸려 있을 때. 우리 사이트 레포의 결제 코드, 초급 6주차의 "운영 DB는 성역" 규칙이 그렇습니다. 사람이 매번 확인하지 않아도 막히게 만듭니다.

CLAUDE.md와 무엇이 다른가

CLAUDE.md 훅
성격 맥락(부탁) 실행(강제)
누가 판단 모델 Claude Code 클라이언트
실패 시 조용히 어긋남 차단·경고·컨텍스트 주입
적합한 것 스타일, 구조, 이유 시점이 정해진 검증·차단·알림

공식 문서도 "작업을 차단하려면 Claude의 결정과 관계없이 PreToolUse 훅을 사용"하라고 안내합니다. CLAUDE.md는 "왜"를, 훅은 "언제 무엇을"을 맡깁니다.

대표적인 훅 이벤트 10종

공식 문서의 이벤트는 30종이 넘습니다. 실무에서 먼저 만나는 10종입니다.

이벤트 시점 대표 용도
SessionStart 세션 시작·재개·/clear·압축·포크 후 브랜치·이슈 컨텍스트 주입, 규칙 재주입
UserPromptSubmit 프롬프트 제출 직후, 모델 처리 전 키워드 감지, 추가 컨텍스트
PreToolUse 도구 실행 전 위험 명령 차단, 입력 수정
PermissionRequest 권한 결정이 필요할 때 자동 허용·거부
PostToolUse 도구 성공 후 포맷, 린트, 로그
PostToolUseFailure 도구 실패 후 실패 알림
Stop 응답 종료 테스트 통과 확인, 미완료면 계속
SubagentStop 서브에이전트 종료 결과 검증
PreCompact 컨텍스트 압축 전 대화 저장
Notification 알림 발생 Slack·데스크톱 알림

이 밖에 SessionEnd, PostCompact, InstructionsLoaded(CLAUDE.md·rules 로드 시), ConfigChange, FileChanged, TaskCompleted 등이 있습니다. 하나 짚어 둘 점은 PreCompact 훅의 stdout은 컨텍스트에 주입되지 않는다는 것입니다. 압축 뒤에 규칙을 다시 넣고 싶으면 SessionStart에 compact matcher를 걸어야 합니다. 우리가 직접 겪고 고친 함정입니다.

훅은 어디에 설정하나

위치 범위
~/.claude/settings.json 모든 프로젝트, 나만
.claude/settings.json 이 프로젝트, 팀 공유
.claude/settings.local.json 이 프로젝트, 나만
관리 정책 조직 전체
플러그인 hooks/hooks.json 플러그인 활성 시
Skill·에이전트 프론트매터 hooks: 그 구성요소가 동작하는 동안

구조는 이벤트 → matcher → 훅 배열입니다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh", "timeout": 30 }
        ]
      }
    ]
  }
}

matcher는 도구 이름(Bash, Edit|Write)이나 정규식(mcp__.*__write.*)입니다. 비우거나 *면 전부 일치합니다. type은 command 외에 prompt(단일 턴 LLM 판단), agent(서브에이전트 검증), http, mcp_tool이 있습니다. if 필드에 "Bash(rm *)" 같은 권한 규칙 문법을 넣으면 그 패턴에 맞는 호출에서만 실행됩니다.

예시 1: 위험 명령 차단

운영 DB를 지우는 명령과 .env 노출을 막는 PreToolUse 훅입니다. stdin으로 들어오는 JSON에서 tool_input.command를 꺼내 검사합니다.

#!/bin/bash
# .claude/hooks/guard.sh  (chmod +x 필요, Windows는 Git Bash 또는 WSL)
CMD=$(jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -Eq 'DROP TABLE|TRUNCATE|rm -rf /|cat .*\.env'; then
  echo "운영 DB·비밀값을 건드리는 명령은 훅에서 차단됩니다: $CMD" >&2
  exit 2
fi
exit 0

종료 코드 2는 "차단"입니다. stderr 내용이 Claude에게 전달되어 다른 방법을 찾게 됩니다. 종료 코드 0은 통과, 그 외 코드는 경고만 띄우고 진행합니다. 종료 코드 대신 JSON으로 더 세밀하게 결정할 수도 있습니다.

jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"운영 DB 보호"}}'

permissionDecision은 allow, deny, ask 중 하나입니다. 훅이 allow를 돌려줘도 settings.json의 deny 규칙은 그 뒤에 다시 평가되므로 deny가 최종입니다.

예시 2: 저장 후 자동 포맷

파일을 고칠 때마다 포맷터를 돌리는 PostToolUse 훅입니다. 우리 원칙대로 성공하면 아무것도 출력하지 않습니다.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs -I{} npx prettier --write {} >/dev/null" }
        ]
      }
    ]
  }
}

PostToolUse는 도구가 이미 실행된 뒤라 차단은 못 하지만, 종료 코드 2로 stderr를 Claude에게 보여 줄 수는 있습니다. 린트 실패를 그대로 넘겨 다음 턴에서 고치게 하는 용도입니다.

훅 설계 원칙 4가지

  1. 성공은 조용히, 실패만 시끄럽게. 통과 시 출력 없음, 실패 시만 stderr. 훅 출력은 매우 길면 잘릴 수 있고 매번 컨텍스트를 먹습니다.
  2. 하나의 훅은 하나의 판단만. 차단과 포맷을 한 스크립트에 섞지 않습니다. 일치하는 훅은 병렬로 실행되고 같은 명령은 자동으로 중복 제거되므로 쪼개도 비용이 늘지 않습니다.
  3. 경로는 ${CLAUDE_PROJECT_DIR}로. 훅은 현재 디렉터리에서 실행되므로 상대 경로는 깨집니다.
  4. 타임아웃을 짧게 명시. 기본값은 command 600초(단 UserPromptSubmit은 30초), prompt 30초, agent 60초입니다. 검사 훅은 10초 안팎이면 충분합니다.

보안상 주의할 점

훅은 내 계정 권한으로 아무 셸 명령이나 실행합니다. 프로젝트 .claude/settings.json에 훅이 있는 저장소를 클론하면 그 훅이 내 컴퓨터에서 돌 수 있습니다. 그래서 Claude Code는 프로젝트 설정을 적용하기 전에 워크스페이스 신뢰 대화를 거칩니다. 모르는 저장소의 훅은 열어 보고 신뢰합니다. 스크립트 안에서 비밀값을 하드코딩하지 않고, http 타입은 헤더에 넣을 환경 변수를 allowedEnvVars로 명시합니다. 훅은 /dev/tty에 접근할 수 없으므로 대화형 입력을 기대하는 스크립트는 실패합니다.

훅이 동작하지 않을 때 점검 순서

  1. /hooks로 훅이 목록에 있는지, 출처 파일이 무엇인지 봅니다.
  2. matcher가 정규 도구 이름과 맞는지 확인합니다. 화면에 Stop Task로 보여도 이름은 TaskStop입니다.
  3. 스크립트에 실행 권한이 있는지(chmod +x), 셸 프로필이 시작 시 텍스트를 찍어 JSON 파싱을 깨지 않는지 봅니다.
  4. claude --debug로 실행 로그와 종료 코드를 확인합니다.
  5. 종료 코드가 0인데 stdout JSON이 잘못돼 무시되는지 jq .로 검증합니다.
  6. 타임아웃에 걸리는지 봅니다. 느린 훅은 /doctor가 잡아 줍니다.

Hooks 코스: 이어서 읽을 글 10편

훅을 본격적으로 다루는 Hooks 코스는 아래 순서로 읽습니다. 설정 파일 위치와 매처·종료 코드부터 시작해, 이벤트별 훅(SessionStart·PreToolUse/PostToolUse·UserPromptSubmit·Stop·SubagentStop)을 한 편씩 다룹니다.

  1. Hooks 설정 방법: settings.json과 /hooks
  2. Hooks 매처: 와일드카드와 패턴 지정
  3. Hooks 종료 코드로 도구 실행 차단하기
  4. Hooks 환경 변수: CLAUDE_PROJECT_DIR과 CLAUDE_ENV_FILE
  5. 비동기 실행 async:true, 훅을 기다리지 않기
  6. SessionStart 훅으로 초기화 자동화
  7. PreToolUse·PostToolUse 훅: 도구 실행 전후 처리
  8. UserPromptSubmit 훅: 프롬프트 가공과 기밀 정보 차단
  9. Stop 훅으로 완료 알림과 로그 자동화
  10. SubagentStop 훅: 서브에이전트 완료 시 처리

정리

  • 두 번 반복된 실수, 정해진 시점, 돈과 데이터가 걸린 곳은 훅입니다.
  • PreToolUse 종료 코드 2가 차단이며 allow 규칙보다 우선합니다.
  • 성공은 조용히, 실패만 시끄럽게. 경로는 ${CLAUDE_PROJECT_DIR}, 타임아웃은 짧게.
  • 모르는 저장소의 훅은 열어 보고 신뢰합니다.

자주 묻는 질문

CLAUDE.md에 적는 것과 훅으로 거는 것은 무엇이 다른가요?

CLAUDE.md는 모델이 읽고 따르는 맥락이라 지켜질 확률은 높지만 보장은 없습니다. 훅은 Claude Code 클라이언트가 정해진 시점에 실행하는 명령이라 모델 판단과 무관하게 항상 동작합니다. "왜"와 스타일은 CLAUDE.md, "언제 무엇을 반드시"는 훅입니다.

훅 설정은 어느 파일에 넣어야 하나요?

팀이 같이 써야 하면 .claude/settings.json, 나만 쓰면 ~/.claude/settings.json이나 .claude/settings.local.json입니다. 특정 Skill이 도는 동안만 필요하면 그 Skill의 프론트매터 hooks:에 둡니다.

도구 실행을 막을 수 있는 훅은 무엇인가요?

PreToolUse입니다. 종료 코드 2 또는 permissionDecision: "deny" JSON으로 막습니다. PermissionRequest도 권한 결정을 대신할 수 있습니다. PostToolUse는 이미 실행된 뒤라 막지 못하고 결과를 Claude에게 알리는 데 씁니다.

훅이 동작하지 않을 때 무엇부터 확인하나요?

/hooks에 보이는지, matcher 이름이 맞는지, 스크립트 실행 권한이 있는지 순서로 봅니다. 그다음 claude --debug로 종료 코드와 출력 JSON을 확인합니다.

팀에 클로드코드를 도입하려면 실제 업무에 맞춘 설계가 필요합니다. 무료 상담 신청 →