← 모든 글

Claude Code 훅 튜토리얼: 프로덕션 훅 5개를 처음부터 만들기

Part 5 of New to Claude Code

가이드에서: Claude Code Comprehensive Guide

Claude Code는 대부분의 경우 올바른 동작을 선택합니다. 문제는 남은 극소수의 예외 상황입니다. main으로의 force push, 포매터 실행 누락, lint를 통과하지 못하는 코드의 커밋이 그렇습니다. 훅은 Claude의 워크플로 안에 있는 31개 라이프사이클 지점(2026년 8월 기준)에 결정론적인 게이트를 놓아 이런 예외 상황을 없앱니다.1 이 튜토리얼은 프로덕션 수준의 에이전트 시스템을 만드는 AI 엔지니어링 시리즈의 한 편입니다. 훅은 프롬프트를 어떻게 썼는지, 모델이 어떻게 행동하는지와 무관하게 예외 없이 매번 실행됩니다.

TL;DR: 훅은 Claude Code의 라이프사이클 이벤트가 실행시키는 셸 명령입니다.1 PreToolUse 훅은 동작을 검사하고 차단합니다(종료 코드 2 = 차단, 0 = 허용).2 PostToolUse 훅은 사후에 검증하고 포매팅합니다. 설정은 .claude/settings.jsonmatcher(정확한 도구 이름, |로 구분한 목록, 또는 정규식)와 중첩된 hooks 배열로 작성합니다.3 아래 튜토리얼에서는 프로덕션 훅 다섯 개를 만듭니다. 자동 포매터, 보안 게이트, 테스트 러너, 알림, 그리고 커밋 전 품질 검사입니다.

핵심 요약

  • 1인 개발자: 자동 포매터(훅 1)와 보안 게이트(훅 2)부터 시작하세요. 이 두 개만으로 Claude Code에서 가장 흔한 실수를 막을 수 있고, 이후 유지보수도 필요 없습니다.
  • 팀 리드: 훅을 저장소의 .claude/settings.json에 커밋하세요. 팀원 모두가 동일한 안전 게이트와 품질 검사를 자동으로 갖게 됩니다.
  • 보안 엔지니어: 동작을 차단하는 것은 종료 코드 2입니다.2 종료 코드 1은 경고만 남깁니다. 모든 PreToolUse 보안 훅은 반드시 exit 2를 써야 하며, 그러지 않으면 강제력이 전혀 없습니다.

훅이란 무엇인가

훅은 Claude Code 세션 중 특정 라이프사이클 이벤트에서 실행되는 셸 명령입니다. 모델이 해석하는 프롬프트가 아니라, Claude의 동작이 촉발하는 평범한 스크립트로서 LLM 바깥에서 실행됩니다.

네 가지 주요 범주가 대부분의 사용 사례를 덮습니다(Claude Code는 2026년 8월 기준으로 31가지 이벤트를 문서화하고 있습니다).1

  • 세션 이벤트: SessionStart는 세션이 시작될 때, SessionEnd는 세션이 닫힐 때 실행됩니다. Stop은 Claude가 응답을 마칠 때마다 실행됩니다(세션이 끝날 때만이 아닙니다). 준비 작업, 정리 작업, 알림에 쓰세요.
  • 도구 이벤트: PreToolUsePostToolUse는 Claude가 도구를 쓰기 직전과 직후에 실행됩니다(파일 쓰기, bash 명령 실행, 코드 검색 등). 특정 동작을 검사하고 차단할 수 있어서 가장 강력한 훅입니다.
  • 알림 이벤트: Notification은 Claude가 알림을 생성할 때 실행됩니다. Slack, 데스크톱 알림, 로그 시스템으로 경보를 보낼 때 유용합니다.
  • 서브에이전트 이벤트: SubagentStop은 Agent 도구로 띄운 서브에이전트가 작업을 마칠 때 실행됩니다.4 훅은 서브에이전트의 동작에도 걸리므로 안전 게이트가 재귀적으로 적용됩니다.

종료 코드의 의미가 중요합니다.2 0은 성공(계속 진행), 2는 동작 차단, 1은 차단하지 않는 훅 오류로 동작은 그대로 진행됩니다. 보안상 중요한 훅은 게이트를 실제로 강제하려면 반드시 exit 2를 써야 합니다.

사고 모델: 세 가지 보장

훅을 쓰기 전에 스스로에게 물어보세요. 내게 필요한 보장은 어떤 종류인가?

포매팅 보장은 사후에 일관성을 지킵니다. Write/Edit에 걸린 PostToolUse 훅은 파일이 바뀔 때마다 포매터를 실행합니다. 포매터가 전부 정규화하므로 모델의 출력이 어땠는지는 상관없습니다. 이런 훅은 멱등이라 편집마다 실행해도 안전합니다.

안전 보장은 위험한 동작을 실행 전에 막습니다. Bash에 걸린 PreToolUse 훅은 명령을 검사하고 파괴적인 패턴을 종료 코드 2로 차단합니다. 이런 훅은 매칭된 모든 도구 호출을 통과시키는 관문이므로 빨라야 하고(500ms 미만), exit 1은 차단 없이 경고만 하므로 반드시 exit 2를 써야 합니다.

품질 보장은 결정이 내려지는 지점에서 상태를 검증합니다. git commit 명령에 걸린 PreToolUse 훅은 linter나 테스트 스위트를 실행하고, 품질 검사가 실패하면 커밋을 차단합니다. 편집마다 실행되는 포매팅 훅과 달리 품질 훅은 특정 순간에만 실행되므로 부담이 적습니다.

개념적 조상은 Git 훅입니다8: pre-commit, pre-push, post-commit이 정확히 같은 세 가지 역할을 합니다. Claude Code의 훅은 이 패턴을 Git 작업에서 에이전트가 수행하는 모든 도구 동작으로 확장한 것입니다. 이 변화는 모든 훅은 흉터다에서 자세히 다뤘습니다. 모든 훅은 그것이 없어서 무언가 잘못됐기에 존재합니다.


훅 설정의 기본

훅은 설정 파일 안에 둡니다.

  • 프로젝트 수준: 저장소 루트의 .claude/settings.json(팀과 공유)3
  • 사용자 수준: ~/.claude/settings.json(개인 훅, 모든 프로젝트에 전역 적용)3

JSON 구조는 다음과 같습니다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/your/script.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/another-script.sh"
          }
        ]
      }
    ]
  }
}

각 항목은 Bash, Write, Edit, Read, Glob, Grep, Agent 같은 도구 이름을 걸러내는 matcher와, 훅 정의를 담은 hooks 배열로 이뤄집니다. 레퍼런스가 정의하는 matcher의 의미는 이렇습니다. "*", "", 또는 matcher를 생략하면 전부 매칭됩니다. 영문자, 숫자, _, -, |, 쉼표, 공백으로만 이뤄진 값은 정확한 이름 또는 목록으로 취급되므로 Write|Edit는 두 도구 모두에 매칭됩니다(mcp__github__search_code 같은 MCP 도구 이름에서는 밑줄이 중요합니다). 그 밖의 값은 앵커 없는 정규식으로 처리됩니다. 매칭은 대소문자를 구분하므로 bash는 결코 Bash에 매칭되지 않습니다. 각 훅에는 type(셸 명령이면 "command")과 실행할 command를 지정합니다.

등록된 훅은 세션 안에서 읽기 전용 /hooks 브라우저로 확인할 수 있습니다. 훅을 추가하거나 바꾸거나 지우려면 설정 JSON을 직접 편집하세요.5

훅이 실행되면 Claude Code는 컨텍스트를 stdin의 JSON 객체로 전달합니다. 도구 이름, 도구 입력(파일 작업이면 file_path 포함), 세션 메타데이터가 담깁니다.6 스크립트는 보통 jq로 stdin을 읽어 판단을 내립니다. 컨텍스트용 환경 변수도 몇 개 설정됩니다. 경로 확인용 $CLAUDE_PROJECT_DIR, 현재 effort 수준을 담은 $CLAUDE_EFFORT 같은 것들입니다. 다만 파일 경로처럼 도구마다 다른 필드는 오직 stdin으로만 들어옵니다. 도구별 $FILE_PATH 변수 같은 것은 없습니다.


실전 훅 5개

아래 훅은 모두 Claude Code를 주력 개발 도구로 쓰면서 실제로 부딪힌 문제를 풉니다. 예제는 전부 훅 레퍼런스의 올바른 중첩 스키마를 따릅니다7.

1. 파일 편집 시 자동 포매팅

Claude가 쓰는 코드는 기능적으로는 맞지만 가끔 프로젝트의 포매팅 규칙을 깨뜨립니다. 처음에는 “Python 파일을 편집하면 항상 black을 실행하라”를 CLAUDE.md에 넣어봤지만, 이 지시는 80% 정도만 지켜졌습니다. 여러 파일에 걸친 복잡한 변경에 집중하다 보면 포매팅 단계를 건너뛰곤 했습니다. PostToolUse 훅을 쓰면 이 들쭉날쭉함이 완전히 사라집니다. 모델이 무엇을 선택하든, 파일이 기록될 때마다 포매터가 실행됩니다.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path // empty\"); if [[ \"$FILE\" == *.py ]]; then black --quiet \"$FILE\" 2>/dev/null; elif [[ \"$FILE\" == *.js || \"$FILE\" == *.ts ]]; then npx prettier --write \"$FILE\" 2>/dev/null; fi'"
          }
        ]
      }
    ]
  }
}

이 훅은 도구의 JSON 입력을 stdin에서 읽어 jq.tool_input.file_path를 뽑아냅니다. Claude Code는 도구별 환경 변수를 설정하지 않으므로, 파일 경로가 존재하는 곳은 그 stdin 객체뿐입니다. 그다음 확장자를 확인해 알맞은 포매터를 실행합니다. Python 파일에는 black, JavaScript와 TypeScript 파일에는 prettier입니다. 2>/dev/null은 시끄러운 출력을 눌러서 진짜 오류만 보이게 합니다.

규모가 큰 프로젝트라면 인라인 명령을 독립 스크립트로 옮기는 편이 읽기 좋습니다.

2. 위험한 명령을 막는 보안 게이트

Bash 도구에 걸린 PreToolUse 훅은 Claude가 실행하려는 명령을 검사하고, 위험한 패턴에 걸리면 차단합니다. 이 훅의 첫 버전은 리팩터링 세션 중에 Claude가 main으로 force push한 뒤에 썼습니다. (에이전트 자율성이 갖는 더 넓은 함의는 발톱의 해부학인프라로서의 Claude Code에서 다뤘습니다.) 모델은 “변경 사항을 push해줘”라는 요청을 받았고, 브랜치가 갈라져 있었기에 그것을 git push --force origin main으로 해석했습니다. 복구 자체는 몇 초면 끝났지만, 이 사건이 영구적인 게이트를 만들게 했습니다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r \".tool_input.command\"); if echo \"$CMD\" | grep -qE \"rm\\s+-rf\\s+/|git\\s+push\\s+(-f|--force)\\s+(origin\\s+)?main|git\\s+reset\\s+--hard|DROP\\s+TABLE|:\\(\\)\\s*\\{\\s*:\"; then echo \"BLOCKED: Dangerous command detected: $CMD\" >&2; exit 2; fi'"
          }
        ]
      }
    ]
  }
}

이 훅이 종료 코드 2로 끝나면 Claude Code는 대기 중이던 명령을 취소합니다. 오류 메시지는 터미널과 Claude의 컨텍스트 양쪽에 출력되므로, 모델은 왜 동작이 실패했는지 이해하고 더 안전한 대안을 제안합니다.

차단되는 패턴: - rm -rf /(루트에서의 재귀적 삭제) - git push --force maingit push -f main(main 브랜치로의 force push) - git reset --hard(커밋하지 않은 작업 파괴) - DROP TABLE(실수로 인한 데이터베이스 파괴) - 포크 폭탄(:(){ 도입부에 매칭되므로 공백이 있는 형태와 없는 형태를 모두 잡습니다)

이 목록은 각자의 환경에 맞게 손보세요. 프로덕션 데이터베이스를 다룬다면 파괴적인 SQL 패턴이, CLI로 배포한다면 배포 명령 가드가 필요합니다.

3. 변경 후 실행되는 테스트 러너

Claude가 Python 파일을 편집하면 관련 테스트를 자동으로 실행합니다. 테스트를 즉시 돌리면, 이어지는 서너 번의 파일 편집을 거치며 문제가 쌓이기 전에 회귀를 잡아냅니다.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path // empty\"); if [[ \"$FILE\" == *.py && \"$FILE\" != *test_* ]]; then TEST_FILE=\"tests/test_$(basename \"$FILE\")\"; if [[ -f \"$TEST_FILE\" ]]; then if ! OUT=$(python -m pytest \"$TEST_FILE\" -x --tb=short 2>&1); then echo \"TESTS FAILED after editing $FILE:\" >&2; echo \"$OUT\" | tail -20 >&2; exit 2; fi; fi; fi'"
          }
        ]
      }
    ]
  }
}

이 훅은 편집된 파일의 경로를 stdin JSON에서 뽑아내고, 그것이 Python 소스 파일인지(테스트 파일 자신이 아닌지) 확인한 뒤, test_ 접두사 명명 규칙으로 대응하는 테스트 파일을 찾아 있으면 실행합니다. -x 플래그는 첫 실패에서 멈추고, tail -20은 출력을 간결하게 유지합니다. 이 훅을 쓸모 있게 만드는 대목은 실패 시 exit 2를 내는 부분입니다. PostToolUse 훅은 이미 일어난 편집을 되돌리지 못합니다. 하지만 exit 2를 내면 실패한 테스트 출력이 stderr를 통해 Claude에게 전달되고, Claude는 다음 단계로 넘어가기 전에 그 문제를 고칩니다. exit 0인 채로 실패를 출력만 하는 버전은 그 출력을 디버그 로그로 보냅니다. 아무도 보지 않는 곳입니다.

참고: 위 훅은 test_ 접두사 명명을 쓰는 평평한 tests/ 디렉터리를 전제로 합니다. 소스 트리를 그대로 반영하는 프로젝트(예: src/api/users.py에 대응하는 tests/api/test_users.py)라면 TEST_FILE 줄을 다음으로 바꾸세요.

TEST_FILE="tests/$(echo "$FILE" | sed 's|.*/src/||; s|\([^/]*\)\.py$|test_\1.py|')"

테스트 러너 훅은 Claude가 여러 파일을 건드리는 리팩터링 세션에서 특히 값집니다. 즉각적인 피드백이 없으면 오류는 쌓입니다. Claude가 파일 A를 편집해 파일 B의 테스트를 깨뜨리고, 깨진 B의 상태를 전제로 파일 C를 편집합니다. 실패를 발견할 즈음이면 고쳐야 할 파일이 하나가 아니라 셋입니다. 편집할 때마다 테스트를 돌리면 첫 번째 파손을 즉시 잡습니다.

4. Claude가 응답을 마치면 알림 보내기

Claude Code의 한 턴이 몇 분씩 걸리기도 합니다. 터미널을 지켜보는 대신, Claude가 응답을 마치면 알림을 받으세요. (Stop은 모든 응답이 끝날 때 실행됩니다. 실제로 세션이 닫히는 시점의 훅이 필요하면 SessionEnd를 대신 등록하세요.)

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude finished responding\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

위의 macOS 버전은 osascript로 네이티브 알림을 띄웁니다. Linux라면 osascript 줄을 notify-send "Claude Code" "Finished responding"으로 바꾸세요. Slack 알림에는 웹훅을 씁니다.

curl -s -X POST "$SLACK_WEBHOOK_URL" \
  -H 'Content-type: application/json' \
  -d '{"text": "Claude Code finished responding"}'

저는 & <task>(Claude Code의 백그라운드 모드)로 띄운 작업에는 Slack 버전을 쓰고, 대화형 세션은 데스크톱 알림에 맡깁니다.

5. 커밋 전 품질 검사

Claude가 git commit을 실행하기 전에 코드가 lint를 통과하는지 검증합니다. 커밋 전 lint 게이트는 포매팅만으로는 놓치는 문제를 잡습니다. 쓰이지 않는 import, 정의되지 않은 변수, 타입 오류 같은 것들입니다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r \".tool_input.command\"); if echo \"$CMD\" | grep -qE \"^git\\s+commit\"; then if ! LINT_OUTPUT=$(ruff check . --select E,F,W 2>&1); then echo \"LINT FAILED -- fix before committing:\" >&2; echo \"$LINT_OUTPUT\" >&2; exit 2; fi; fi'"
          }
        ]
      }
    ]
  }
}

이 품질 게이트는 Bash 명령이 git commit으로 시작할 때만 작동합니다. 빠른 Python linter인 ruff를 error, pyflakes, warning 규칙으로 실행합니다. 문제가 하나라도 있으면 훅이 커밋을 차단하고(exit 2) Claude가 lint 출력을 보게 되는데, 그러면 대개 문제를 고친 뒤 다시 시도합니다.

품질 검사는 여러 겹으로 쌓을 수 있습니다. 타입 검사에는 mypy, 보안 스캔에는 bandit, 또는 프로젝트 고유의 검증 스크립트를 쓰면 됩니다. Bash 명령에 걸린 PreToolUse 훅은 모든 셸 동작 앞에 프로그래밍 가능한 게이트를 놓아줍니다.


.claude/settings.json의 PreToolUse와 PostToolUse: 레퍼런스

PreToolUse/PostToolUse 설정의 형태를 검색하다 여기 도착했다면, 이 절이 압축판입니다. 두 이벤트 모두 .claude/settings.json(프로젝트) 또는 ~/.claude/settings.json(사용자)의 hooks 키 아래에 중첩됩니다. 스코프는 합쳐지며, 동일한 핸들러는 중복이 제거됩니다. 두 이벤트를 한 블록에 연결하면 이렇습니다.3

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": ".claude/hooks/guard.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": ".claude/hooks/format.sh" }]
      }
    ]
  }
}

계약은 세 줄로 요약됩니다. 두 이벤트 모두 도구 JSON을 stdin으로 전달합니다(Bash는 .tool_input.command, Write/Edit은 .tool_input.file_path. 도구별 환경 변수는 없습니다).6 PreToolUse는 도구 호출 전에 실행되며 exit 2로 그것을 차단할 수 있습니다. PostToolUse는 도구가 성공한 뒤에 실행됩니다. 동작을 되돌릴 수는 없지만, exit 2를 내면 그 stderr가 Claude에게 돌아가고 Claude는 훅이 지적한 것을 고칩니다.2

두 이벤트의 전체 문서, 즉 JSON 출력 필드, permissionDecision, updatedInput, 타임아웃은 공식 레퍼런스인 code.claude.com/docs/en/hooks에 있습니다. 제 Claude Code 가이드의 훅 섹션도 현장에서 검증한 패턴과 함께 같은 범위를 다룹니다.

이벤트 이름을 짐작했다면: 대응표

사람들이 검색하는 훅 이벤트 이름과, Claude Code가 실제로 실행시키는 이벤트의 대응입니다.1

이렇게 짐작했다면… 실제 이벤트
onStart / onSessionStart SessionStart
onFinish / onEnd / onStop Stop(Claude가 각 응답을 마칠 때 실행) 또는 SessionEnd(세션이 닫힐 때)
onToolUse / beforeToolUse PreToolUse
afterToolUse PostToolUse
onPrompt / onUserMessage UserPromptSubmit
onError PostToolUseFailure(도구 오류) 또는 StopFailure(API 오류)

이벤트는 모두 31개입니다. 가이드의 이벤트 표에 전부 정리돼 있습니다.

PreToolUse, PostToolUse, Stop 훅을 한 설정에 담기

가장 많이 검색되는 세 이벤트를 함께 연결한 예입니다. 명령 가드, 포매터, 완료 알림입니다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": ".claude/hooks/guard-bash.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "bash -c 'FILE=$(jq -r \".tool_input.file_path // empty\"); [[ \"$FILE\" == *.py ]] && black --quiet \"$FILE\" || true'" }]
      }
    ],
    "Stop": [
      {
        "hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude finished responding\" with title \"Claude Code\"'" }]
      }
    ]
  }
}

guard-bash.sh는 훅 2의 보안 게이트를 독립 스크립트로 옮긴 것입니다. 훅 2의 bash -c 한 줄짜리 명령의 본문(바깥 작은따옴표 사이에 있는 전부)을 .claude/hooks/guard-bash.sh로 저장하고, #!/bin/bash 셔뱅을 붙인 뒤 chmod +x 하세요. 아니면 Claude Code 훅 완전 해설에서 같은 이름의 완성된 스크립트를 그대로 가져와도 됩니다. 각 이벤트는 저마다의 의미를 그대로 유지합니다. PreToolUse 가드는 명령을 거부할 수 있고(exit 2), PostToolUse 포매터는 매칭된 편집마다 실행되며, Stop 훅은 응답이 끝날 때마다 실행됩니다. Stop에는 matcher가 필요 없습니다. 도구 이벤트가 아니라서 걸러낼 대상이 없기 때문입니다.1


훅 디버깅 팁

훅은 생각보다 자주 소리 없이 실패합니다. 제가 디버깅할 때 쓰는 다섯 가지 방법입니다.

  1. 스크립트를 먼저 단독으로 테스트하세요. 샘플 JSON을 직접 스크립트에 흘려보냅니다. echo '{"tool_input":{"command":"git commit -m test"}}' | bash your-hook.sh처럼요. Claude Code 밖에서 실패하는 것은 안에서도 실패합니다.
  2. stderr가 실제로 어디로 가는지 아세요. stderr가 Claude의 컨텍스트에 닿는 것은 훅이 exit 2로 끝날 때뿐입니다. exit 0이면 디버그 로그로 가고, 그 밖의 0이 아닌 종료에서는 트랜스크립트에 훅 오류 알림만 뜹니다. 개발 중에는 claude --debug(세션 도중이라면 /debug)를 실행하고, exit 0인 훅의 출력이 떨어지는 디버그 로그를 지켜보세요.
  3. jq 실패를 주의하세요. JSON 경로가 틀리면 jq9는 조용히 null을 돌려주고 조건문은 아무것도 매칭하지 않습니다. jq 표현식은 실제 도구 입력으로 테스트하세요.
  4. 종료 코드를 검증하세요. exit 2는 동작을 차단하고, exit 1은 경고만 합니다. 실수로 exit 1을 쓴 PreToolUse 훅은 작동하는 것처럼 보이지만 강제력은 0입니다. 기본은 허용(기본값 exit 0)으로 두고, exit 2는 차단하려는 특정 패턴에만 쓰세요.
  5. 훅을 빠르게 유지하세요. 훅은 동기적으로 실행됩니다. 5초 걸리는 훅은 매칭된 도구 사용마다 5초를 더합니다. 저는 모든 훅을 2초 이내, 이상적으로는 500밀리초 이내로 유지합니다.

가장 흔한 훅 실수: 보안 게이트를 exit 2가 아니라 exit 1로 쓰는 것입니다. 경고 메시지가 터미널에 출력되니 테스트할 때는 작동하는 것처럼 보입니다. 하지만 exit 1은 차단하지 않는 경고입니다. 위험한 명령은 그대로 실행됩니다. 저는 이 실수를 서로 다른 세 팀의 훅 설정에서 봤고, 그들 모두 force push를 막았다고 믿고 있었습니다. 모든 보안 훅은 차단 대상 패턴을 실제로 발생시켜, 경고만 뜬 것이 아니라 동작이 정말로 막혔는지 확인하세요.


다음 단계

이 다섯 개의 훅은 기본기를 덮습니다. 포매팅, 보안, 테스트, 알림, 품질 게이트입니다. 이 패턴들이 손에 익으면 컨텍스트 주입(세션 시작 시 프로젝트 고유의 지침을 넣기), 재귀 가드(서브에이전트의 무한 루프 막기), 워크플로 오케스트레이션(여러 단계의 과정을 엮기)을 위한 훅도 만들 수 있습니다.

훅 아키텍처, 31개 이벤트 전체 라이프사이클, 고급 패턴은 Claude Code 가이드의 훅 섹션이나, 이벤트별로 짚어가는 Claude Code 훅 완전 해설을 보세요.

프로덕션에서 돌리는 훅 95개의 사연은 Claude Code 훅: 제 훅 95개가 각각 존재하는 이유에 썼습니다. 각 훅을 만들게 한 사건들을 다룹니다.


참고 문헌


FAQ

훅이 Claude Code의 명령 실행을 막을 수 있나요?

네. PreToolUse 훅은 종료 코드 2로 끝나면서 어떤 도구 동작이든 차단합니다. Claude Code는 대기 중이던 동작을 취소하고 훅의 stderr 출력을 모델에 보여줍니다. exit 1은 차단하지 않는 훅 오류이고 동작은 그대로 진행됩니다. 종료 코드의 이 구분이 중요합니다. 모든 보안 훅은 exit 1이 아니라 exit 2를 써야 합니다.2 Claude는 거부 사유를 읽고 더 안전한 대안을 제안합니다.

훅 설정 파일은 어디에 두나요?

훅 설정은 프로젝트 수준이라면 .claude/settings.json(저장소에 커밋해 팀과 공유), 사용자 수준이라면 ~/.claude/settings.json(개인용, 모든 프로젝트에 적용)에 둡니다. 둘 다 있으면 훅은 덮어쓰기가 아니라 합쳐집니다. 모든 스코프에서 매칭되는 훅이 전부 실행되고, 동일한 핸들러는 중복이 제거됩니다. 스크립트 파일은 작업 디렉터리 문제를 피하려면 절대 경로로 지정하기를 권합니다.

훅이 서브에이전트에서도 동작하나요?

네. 훅은 서브에이전트의 동작에도 걸립니다.4 Claude가 Agent 도구로 서브에이전트를 띄우면, 그 서브에이전트가 쓰는 모든 도구에 대해 PreToolUse와 PostToolUse 훅이 실행됩니다. 재귀적으로 강제되지 않는다면 서브에이전트가 안전 게이트를 우회할 수 있습니다. SubagentStop 이벤트를 쓰면 서브에이전트가 작업을 마칠 때 정리나 검증을 돌릴 수 있습니다.4

훅이 몇 개부터 너무 많은가요?

제약은 개수가 아니라 성능입니다. 각 훅이 동기적으로 실행되므로 훅의 총 실행 시간이 매칭된 도구 호출마다 더해집니다. 저는 사용자 수준과 프로젝트 수준을 합쳐 95개의 훅을 돌리지만 체감할 만한 지연은 없습니다. 훅 하나하나가 200ms 안에 끝나기 때문입니다. 제가 지켜보는 기준은 이렇습니다. PostToolUse 훅이 파일 편집마다 500ms를 넘게 더한다면 세션이 굼뜨게 느껴집니다. 배포하기 전에 time으로 훅을 프로파일링하세요. 빠른 훅 열 개가 느린 훅 두 개보다 낫습니다.


  1. Anthropic, “Hooks reference — Hook events”. code.claude.com/docs/en/hooks#hook-events 

  2. Anthropic, “Hooks reference — Exit code output”. code.claude.com/docs/en/hooks#exit-code-output 

  3. Anthropic, “Hooks reference — Configuration”. code.claude.com/docs/en/hooks#configuration 

  4. Anthropic, “Hooks reference — Hook events”(SubagentStart/SubagentStop). code.claude.com/docs/en/hooks#hook-events 

  5. Anthropic, “Hooks reference — Configuration”(/hooks 메뉴). code.claude.com/docs/en/hooks#configuration 

  6. Anthropic, “Hooks reference — Hook input and output”. code.claude.com/docs/en/hooks#hook-input-and-output 

  7. Anthropic, “Hooks reference — Configuration”(중첩 훅 스키마). code.claude.com/docs/en/hooks#configuration 

  8. Git 문서, “Customizing Git: Git Hooks”. git-scm.com/book/en/v2/Customizing-Git-Git-Hooks 

  9. jq 매뉴얼, “Command-line JSON processor”. jqlang.github.io/jq/manual 

관련 게시물

Claude Code 훅 완전 해설: 에이전트를 감싸는 결정론적 계층

Claude Code 훅은 라이프사이클 이벤트에서 셸 명령을 반드시 실행합니다. 모든 이벤트, 종료 코드의 의미, 그리고 Prettier부터 Stop 게이트까지 다섯 가지 패턴을 정리했습니다.

17 분 소요

Claude Code Hooks: 95개의 Hook이 각각 존재하는 이유

Claude Code용으로 95개의 hook을 만들었습니다. 각각은 무언가 잘못되었기 때문에 존재합니다. 그 기원 이야기와 거기서 발생한 아키텍처를 소개합니다.

8 분 소요