본문 바로가기

Claude

Claude Code Hooks 완전가이드 — 프롬프트 요청이 아닌 보장된 실행

반응형

Claude에게 "파일 수정 후 포맷해줘"라고 프롬프트로 요청하는 것과 Hooks로 PostToolUse에 포매터를 걸어두는 것은 다릅니다. 전자는 요청입니다. 후자는 보장입니다.


 

핵심 요약 → Hooks = 에이전트 루프 특정 시점에 자동 실행되는 셸 커맨드·HTTP·프롬프트·에이전트 → 2026년 5월 기준 v2.1.141+ → 27개 라이프사이클 이벤트, 5가지 핸들러 타입 → 가장 중요한 이벤트: PreToolUse (실행 전 차단), PostToolUse (실행 후 반응) → 핵심 exit code: 0 = 계속, 2 = 차단 (exit 1은 차단 아님 — 가장 흔한 실수) → stdin으로 JSON 컨텍스트 수신 → stdout/stderr + exit code로 Claude에게 피드백 → 설정 위치: ~/.claude/settings.json (글로벌) or .claude/settings.json (프로젝트) → 실전 80%는 PreToolUse + PostToolUse로 해결


1. 기본 구조 — 설정 파일부터

// .claude/settings.json (프로젝트 공유)
// .claude/settings.local.json (개인 전용, .gitignore 추가 권장)
// ~/.claude/settings.json (전역)

{
  "hooks": {
    "이벤트명": [
      {
        "matcher": "툴이름_정규식",  // 생략 시 모든 툴에 적용
        "hooks": [
          {
            "type": "command",        // command | http | prompt | agent
            "command": "실행할 커맨드",
            "timeout": 30,            // 초 단위 (기본 600)
            "async": false,           // true면 블로킹 없이 백그라운드 실행
            "statusMessage": "검사 중..." // Claude Code UI 스피너 메시지
          }
        ]
      }
    ]
  }
}

2. 27개 이벤트 전체 목록 + 용도

2026년 5월 v2.1.141+ 기준 27개 이벤트: SessionStart, Setup, SessionEnd, UserPromptSubmit, UserPromptExpansion, Stop, StopFailure, PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied, SubagentStart, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, Notification, Elicitation, ElicitationResult.

# 실전에서 자주 쓰는 이벤트 Top 8

PreToolUse        ← 가장 강력. 툴 실행 전 차단 가능
PostToolUse       ← 툴 실행 후 반응 (포매팅, 로깅, 알림)
SessionStart      ← 세션 시작 시 컨텍스트 주입
UserPromptSubmit  ← 프롬프트 제출 전 전처리
Stop              ← Claude 응답 완료 후 정리·검증
Notification      ← Claude 알림 → Slack/Discord 연동
SubagentStop      ← 서브에이전트 완료 후 후처리
PostCompact       ← 컨텍스트 압축 후 요약 저장

# 나머지는 고급 케이스
WorktreeCreate/Remove  → Git worktree 기반 병렬 에이전트
PreCompact             → 압축 전 중요 내용 백업
PermissionRequest      → 권한 요청 자동 승인/거부

3. Exit Code — 가장 흔한 실수

exit 2 (PreToolUse) = 툴 호출 차단. stderr 출력이 Claude에게 이유로 전달됩니다. exit non-zero (PostToolUse) = 경고 로깅, 하지만 툴 실행은 이미 완료 — 되돌릴 수 없습니다.

#!/usr/bin/env bash
# exit code 의미 완전 정리

# exit 0  → 정상. 계속 진행
# exit 2  → 차단 (PreToolUse에서만 유효). stderr가 Claude에게 전달됨
# exit 1  → ⚠️ 함정! Unix 관례상 실패지만 Claude Code는 차단 안 함
#            PostToolUse에서는 경고 로그만 남김
# async: true 훅에서는 exit code 무시 (블로킹 없음)

# ✅ 올바른 차단 패턴
if [[ "$위험한_조건" == "true" ]]; then
    echo "이유 설명" >&2   # stderr → Claude가 읽음
    exit 2                 # 차단!
fi

# ❌ 흔한 실수
if [[ "$위험한_조건" == "true" ]]; then
    echo "이유 설명" >&2
    exit 1                 # 차단 안 됨! 그냥 경고만
fi

4. PreToolUse — 위험한 명령 차단

#!/usr/bin/env bash
# .claude/hooks/guard-dangerous.sh
# PreToolUse 훅 — 위험한 Bash 명령 차단

# stdin에서 JSON 이벤트 수신
INPUT=$(cat)

# 툴이 Bash가 아니면 통과
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
[[ "$TOOL_NAME" != "Bash" ]] && exit 0

# 실행하려는 명령 추출
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# 위험 패턴 목록
DANGEROUS_PATTERNS=(
    "rm -rf /"
    "rm -rf \*"
    "dd if=/dev/zero"
    ":(){:|:&};:"          # Fork bomb
    "chmod -R 777 /"
    "curl.*| bash"         # 외부 스크립트 직접 실행
    "wget.*| sh"
    "DROP TABLE"
    "DROP DATABASE"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
    if echo "$COMMAND" | grep -qiE "$pattern"; then
        echo "🚫 차단됨: 위험한 명령 패턴 감지 — '$pattern'" >&2
        echo "명령: $COMMAND" >&2
        exit 2  # 차단
    fi
done

# 프로덕션 DB 접근 차단
if echo "$COMMAND" | grep -qE "psql.*prod|mysql.*prod"; then
    echo "🚫 차단됨: 프로덕션 DB 직접 접근 금지" >&2
    exit 2
fi

exit 0  # 통과
// settings.json 등록
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": "bash .claude/hooks/guard-dangerous.sh",
          "timeout": 5
        }]
      }
    ]
  }
}

5. PostToolUse — 파일 수정 후 자동 포매팅

#!/usr/bin/env bash
# .claude/hooks/auto-format.sh
# PostToolUse 훅 — 파일 수정 시 자동 포맷

INPUT=$(cat)

# 파일 경로 추출
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[[ -z "$FILE" || ! -f "$FILE" ]] && exit 0

# 확장자별 포매터 적용
case "$FILE" in
    *.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.html|*.md)
        npx --no-install prettier --write "$FILE" 2>/dev/null
        echo "✅ Prettier 적용: $FILE"
        ;;
    *.py)
        python -m black "$FILE" 2>/dev/null
        python -m isort "$FILE" 2>/dev/null
        echo "✅ Black + isort 적용: $FILE"
        ;;
    *.go)
        gofmt -w "$FILE" 2>/dev/null
        echo "✅ gofmt 적용: $FILE"
        ;;
    *.rs)
        rustfmt "$FILE" 2>/dev/null
        echo "✅ rustfmt 적용: $FILE"
        ;;
esac

exit 0
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [{
          "type": "command",
          "command": "bash .claude/hooks/auto-format.sh"
        }]
      }
    ]
  }
}

6. SessionStart — 컨텍스트 자동 주입

#!/usr/bin/env bash
# .claude/hooks/inject-context.sh
# SessionStart 훅 — 세션 시작 시 프로젝트 컨텍스트 자동 주입
# JSON structured output으로 additionalContext 전달

INPUT=$(cat)

# 프로젝트 정보 수집
NODE_VERSION=$(node --version 2>/dev/null || echo "미설치")
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
OPEN_PRS=$(gh pr list --json number,title 2>/dev/null | jq length || echo "0")
FAILED_TESTS=$(npm test --passWithNoTests 2>&1 | grep -c "FAIL" || echo "0")

# JSON structured output으로 컨텍스트 주입
# Claude Code가 자동으로 대화 컨텍스트에 추가함
cat << EOF
{
  "additionalContext": "## 프로젝트 현황 (자동 주입)\n- Node: $NODE_VERSION\n- 브랜치: $GIT_BRANCH\n- 열린 PR: ${OPEN_PRS}개\n- 실패한 테스트: ${FAILED_TESTS}개\n\n이 정보를 바탕으로 작업하세요."
}
EOF

exit 0
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [{
          "type": "command",
          "command": "bash .claude/hooks/inject-context.sh",
          "statusMessage": "프로젝트 컨텍스트 로딩 중..."
        }]
      }
    ]
  }
}

7. 4가지 핸들러 타입 실전 비교

// 타입 1: command — 90% 케이스
{
  "type": "command",
  "command": "bash .claude/hooks/format.sh",
  "timeout": 30
}

// 타입 2: http — 외부 웹훅, Slack/Discord 알림
{
  "type": "http",
  "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
  "timeout": 10,
  "async": true  // 알림은 비동기로 (Claude 차단하지 않음)
}

// 타입 3: prompt — AI 판단이 필요한 경우 (Haiku 기본값)
// $ARGUMENTS에 훅 입력 JSON이 주입됨
{
  "type": "prompt",
  "prompt": "다음 파일 수정이 보안 취약점을 야기하는지 판단하세요: $ARGUMENTS\n'allow' 또는 'deny: 이유'로만 응답하세요."
}

// 타입 4: agent — 복잡한 검증이 필요할 때 서브에이전트 실행
// Read, Grep, Glob 툴 접근 가능
{
  "type": "agent",
  "prompt": "변경된 파일의 테스트 커버리지가 80% 이상인지 확인하세요. 미달이면 부족한 테스트 목록을 제시하세요.",
  "tools": ["Read", "Grep", "Glob"]
}

8. Stop 훅 — 응답 완료 후 테스트 자동 실행

#!/usr/bin/env bash
# .claude/hooks/run-tests-on-stop.sh
# Stop 훅 — Claude 응답 완료 시 관련 테스트 자동 실행

INPUT=$(cat)

# 마지막으로 수정된 파일들 추출 (transcript에서)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty')
[[ -z "$TRANSCRIPT" ]] && exit 0

# 최근 수정된 .ts/.tsx 파일 탐색
MODIFIED_FILES=$(cat "$TRANSCRIPT" 2>/dev/null | \
    jq -r 'select(.type == "tool_result" and .tool == "Edit") | .file_path' 2>/dev/null | \
    grep -E '\.(ts|tsx|js|jsx)$' | sort -u)

[[ -z "$MODIFIED_FILES" ]] && exit 0

# 관련 테스트 파일 실행
echo "🧪 수정된 파일 감지 → 관련 테스트 실행 중..."
for file in $MODIFIED_FILES; do
    TEST_FILE="${file%.ts}.test.ts"
    if [[ -f "$TEST_FILE" ]]; then
        npx jest "$TEST_FILE" --passWithNoTests 2>&1 | tail -5
    fi
done

exit 0
{
  "hooks": {
    "Stop": [
      {
        "hooks": [{
          "type": "command",
          "command": "bash .claude/hooks/run-tests-on-stop.sh",
          "async": true,   // 비동기: 테스트가 Claude를 차단하지 않음
          "timeout": 120
        }]
      }
    ]
  }
}

9. Notification + HTTP — Slack 연동

// settings.json
{
  "hooks": {
    "Notification": [
      {
        "hooks": [{
          "type": "http",
          "url": "https://hooks.slack.com/services/XXX/YYY/ZZZ",
          "timeout": 10,
          "async": true
        }]
      }
    ],
    "Stop": [
      {
        "hooks": [{
          "type": "http",
          "url": "https://your-api.com/claude-complete",
          "async": true
        }]
      }
    ]
  }
}
#!/usr/bin/env bash
# HTTP 훅 수신 서버 (간단한 Express 예시)
# 실제 배포에선 ngrok 또는 내부 서버 사용

# Slack 웹훅은 HTTP 훅이 직접 POST
# Claude가 전달하는 JSON body:
# {
#   "session_id": "abc-123",
#   "hook_event_name": "Notification",
#   "message": "Claude의 알림 메시지",
#   "cwd": "/path/to/project"
# }

# → Slack incoming webhook 형식과 맞지 않으므로
#   중간 서버 또는 Zapier/Make로 변환 필요

10. 팀 공유 훅 설정 — .claude 디렉토리 구조

# 권장 프로젝트 구조

my-project/
├── .claude/
│   ├── settings.json          # 팀 공유 훅 설정 (git 추적)
│   ├── settings.local.json    # 개인 설정 (.gitignore에 추가)
│   └── hooks/
│       ├── guard-dangerous.sh # 위험 명령 차단
│       ├── auto-format.sh     # 자동 포매팅
│       ├── inject-context.sh  # 컨텍스트 주입
│       ├── run-tests.sh       # 테스트 자동 실행
│       └── notify-slack.sh    # Slack 알림
├── CLAUDE.md                  # Claude 행동 지침
└── ...

# .gitignore에 추가
.claude/settings.local.json
// 팀 공유 settings.json 완성본
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": "bash .claude/hooks/guard-dangerous.sh",
          "timeout": 5,
          "statusMessage": "보안 검사 중..."
        }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [{
          "type": "command",
          "command": "bash .claude/hooks/auto-format.sh",
          "timeout": 30
        }]
      }
    ],
    "SessionStart": [
      {
        "hooks": [{
          "type": "command",
          "command": "bash .claude/hooks/inject-context.sh",
          "statusMessage": "컨텍스트 로딩 중..."
        }]
      }
    ],
    "Stop": [
      {
        "hooks": [{
          "type": "command",
          "command": "bash .claude/hooks/run-tests.sh",
          "async": true,
          "timeout": 120
        }]
      }
    ]
  }
}

디버깅 팁

# 훅 디버깅 — 직접 stdin 테스트

# 실제 훅이 받는 JSON 형태 시뮬레이션
echo '{
  "session_id": "test-123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /"
  },
  "cwd": "/my-project"
}' | bash .claude/hooks/guard-dangerous.sh

echo "exit code: $?"  # 2여야 차단 확인

# /hooks 명령으로 현재 훅 상태 확인
# Claude Code 내에서: /hooks
# → 등록된 이벤트별 훅 목록 표시

# 전체 비활성화 (디버깅용)
# settings.json에 추가:
# "disableAllHooks": true

결론

Hooks가 프롬프트 요청보다 나은 이유

  • 프롬프트는 "요청" → Claude가 잊거나 무시할 수 있음
  • Hooks는 "보장" → 조건 충족 시 반드시 실행

지금 당장 설정할 3가지

  1. PreToolUse + Bash matcher → 위험 명령 차단 (exit 2)
  2. PostToolUse + Edit|Write matcher → 자동 포매팅
  3. SessionStart → 프로젝트 컨텍스트 자동 주입

반드시 기억할 것

  • 차단은 exit 2 — exit 1은 차단 안 됨 (가장 흔한 실수)
  • PostToolUse는 관찰·반응만 가능 — 이미 실행된 툴은 되돌릴 수 없음
  • 알림·로깅은 async: true — Claude를 차단하지 않도록

 

반응형