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가지
- PreToolUse + Bash matcher → 위험 명령 차단 (exit 2)
- PostToolUse + Edit|Write matcher → 자동 포매팅
- SessionStart → 프로젝트 컨텍스트 자동 주입
❌ 반드시 기억할 것
- 차단은 exit 2 — exit 1은 차단 안 됨 (가장 흔한 실수)
- PostToolUse는 관찰·반응만 가능 — 이미 실행된 툴은 되돌릴 수 없음
- 알림·로깅은 async: true — Claude를 차단하지 않도록
'Claude' 카테고리의 다른 글
| Claude Opus 4.8 — 69.2: GPT-5.5가 SWE-bench Pro 58.6%로 정상을 노리던 그 자리, Anthropic이 41일 만에 답했습니다. (0) | 2026.06.01 |
|---|---|
| LLM 배치 처리 실전 — Anthropic Message Batches API로 비용 50% 절감 (0) | 2026.05.29 |
| 일본 정부 + 3대 메가뱅크 Claude Mythos 도입 — 왜 하필 일본이 첫 번째 비(非)영미권 파트너인가 (1) | 2026.05.29 |
| Anthropic이 월스트리트를 노린다 — 10개 금융 에이전트 + $1.5B JV, 무엇이 바뀌나 (0) | 2026.05.28 |
| Anthropic이 공개를 거부한 AI — Claude Mythos 완전 분석 (0) | 2026.05.27 |