AI 코딩 도구 설치·레퍼런스 (3/7) — Claude Code 훅(Hooks) 완전 가이드
CLAUDE.md가 권고라면 훅은 강제다. SessionStart · PreToolUse · PostToolUse · PreCompact 실전
핵심 요약
- 대상 독자: 훅 개념은 들어봤지만 실제로 쓴 적은 없는 Claude Code 사용자.
- 얻을 것: 공식
/hooks레퍼런스 기준 25+ 생명주기 이벤트의 구조, 설정 위치 4곳, 핵심 4종 이벤트를 실제 동작하는 스크립트 3개로 보이기, exit code 함정, matcher 문법, 디버깅 요령. - 선행 조건: Claude Code 설치 완료(설치 완전판), CLAUDE.md 기초(작성 가이드).
1. 훅은 CLAUDE.md와 무엇이 다른가
| CLAUDE.md | 훅(Hook) | |
|---|---|---|
| 성격 | 권고 (advisory) | 강제 (deterministic) |
| 주입 지점 | 시스템 프롬프트 뒤 user message로 | Claude 동작 전후에 shell command 실행 |
| 따라질 확률 | 높지만 보장 없음 | Exit code 2면 무조건 차단 |
| 적합한 용도 | 코드 스타일, 아키텍처 설명 | "절대 건드리면 안 되는 경로", "매 편집 후 포맷 실행" 등 예외 없는 규칙 |
CLAUDE.md에 "절대 쓰기 금지"라고 적어도 Claude가 가끔 어긴다. 훅으로 걸면 어길 수 없다. 훅은 Claude가 아니라 Claude Code CLI가 직접 실행하므로 LLM의 판단을 우회한다.
2. 훅을 어디에 정의하는가
settings.json 또는 스킬/에이전트/플러그인 프론트매터에서 정의할 수 있다.
| 위치 | 범위 | 공유 가능 |
|---|---|---|
~/.claude/settings.json |
모든 프로젝트 (개인) | ❌ |
.claude/settings.json |
현재 프로젝트 | ✅ (git에 commit) |
.claude/settings.local.json |
현재 프로젝트 (개인 전용) | ❌ (.gitignore) |
| Managed policy settings | 조직 전체 | ✅ (IT 배포) |
Plugin hooks/hooks.json |
플러그인 활성 시 | ✅ |
| Skill/Agent frontmatter | 해당 스킬/에이전트 활성 동안만 | ✅ |
전부 비활성화는 "disableAllHooks": true. 다만 managed hooks는 사용자 설정으로 끌 수 없다 (정책 보장).
3. 생명주기 이벤트 — 전체 25+ 개
공식 레퍼런스가 정의한 훅 이벤트를 용도별로 묶으면 다음과 같다.
3.1 세션 레벨
SessionStart— 세션 시작/재개 시SessionEnd— 세션 종료 시
3.2 턴 레벨
UserPromptSubmit— 사용자 프롬프트 제출 직후 (Claude 처리 전)UserPromptExpansion— 슬래시 명령이 프롬프트로 전개될 때Stop— Claude 응답 종료 시StopFailure— API 에러로 종료 시
3.3 에이전트 루프 레벨 (가장 자주 쓰는 카테고리)
PreToolUse— 도구 실행 직전 (차단 가능)PermissionRequest— 권한 다이얼로그 표시 시PermissionDenied— auto-mode 분류기가 차단한 경우PostToolUse— 도구 실행 성공 후PostToolUseFailure— 도구 실행 실패 후PostToolBatch— 병렬 도구 호출 전체 해소 후SubagentStart/SubagentStop— 서브에이전트 시작/종료
3.4 컨텍스트 & 설정
InstructionsLoaded— CLAUDE.md 또는.claude/rules/*.md로드 시ConfigChange— 설정 파일 변경 시CwdChanged— 작업 디렉터리 변경 시FileChanged— 감시 대상 파일이 외부에서 바뀔 때
3.5 압축 & worktree
PreCompact/PostCompact—/compact전후WorktreeCreate/WorktreeRemove— worktree 생성/삭제
3.6 기타
TeammateIdle— 에이전트 팀 teammate가 idle 되기 직전Notification— Claude Code가 알림 전송 시TaskCreated/TaskCompleted— 태스크 생성/완료Elicitation/ElicitationResult— MCP 서버가 사용자 입력 요청 시
이 글은 가장 많이 쓰는 4종에 집중한다: SessionStart, PreToolUse, PostToolUse, PreCompact.
4. 기본 구조
.claude/settings.json의 뼈대:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-bash.sh",
"timeout": 600
}
]
}
]
}
}
구성 요소:
- Event 키 (PreToolUse, SessionStart 등) — 여러 개 등록 가능
- matcher — 해당 이벤트가 언제 발화할지 필터링
- hooks 배열 — 실제 실행될 핸들러 목록
- type — command / http / mcp_tool / prompt / agent 5종
4.1 matcher 문법 (중요)
| 값 | 해석 | 예 |
|---|---|---|
"*", "", 생략 |
모든 이벤트 매칭 | 항상 발화 |
영문·숫자·_·|만 사용 |
정확 일치 또는 pipe 리스트 | Bash 또는 Edit\|Write |
| 그 외 문자 포함 | JavaScript 정규식 | ^Notebook 또는 mcp__memory__.* |
이벤트 종류마다 매칭 기준이 다르다:
| 이벤트 | 매칭 기준 |
|---|---|
PreToolUse/PostToolUse/PermissionRequest/PermissionDenied |
도구 이름 (Bash, Edit|Write, mcp__.*) |
SessionStart |
startup / resume / clear / compact |
SessionEnd |
clear / resume / logout / other |
SubagentStart/SubagentStop |
에이전트 타입 (Explore, Plan, Bash…) |
PreCompact/PostCompact |
manual / auto |
InstructionsLoaded |
session_start / nested_traversal / path_glob_match |
UserPromptSubmit/Stop 등 |
matcher 없음 (항상 발화) |
4.2 type — 5가지 핸들러
| 타입 | 용도 |
|---|---|
command |
shell 명령 실행 (기본) |
http |
HTTP POST 요청 |
mcp_tool |
MCP 서버의 도구 호출 |
prompt |
Claude 프롬프트로 판단 위임 |
agent |
서브에이전트로 판단 위임 (실험) |
이 글은 command 타입만 다룬다 (나머지는 공식 /hooks 레퍼런스 참고).
5. 핵심 4종 실전
5.1 SessionStart — 환경 변수 주입
세션 시작 시 환경변수를 세션 전역으로 주입할 수 있다. CLAUDE_ENV_FILE에 export 문을 쓰면 된다.
.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh"
}
]
}
]
}
}
.claude/hooks/session-start.sh:
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi
RECENT_CHANGES=$(git log --oneline -5 2>/dev/null || echo "no git")
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Recent commits:\n$RECENT_CHANGES"
}
}
EOF
exit 0
실행 권한 부여:
chmod +x .claude/hooks/session-start.sh
additionalContext는 세션 시작 프롬프트에 추가 컨텍스트로 주입된다. 10,000자 상한.
CLAUDE_ENV_FILE은SessionStart,CwdChanged,FileChanged세 이벤트에서만 사용 가능.
5.2 PreToolUse — 위험 명령 차단
공식 레퍼런스 예시를 그대로 쓴다. rm -rf 패턴이 들어간 Bash 명령을 차단한다.
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
.claude/hooks/block-rm.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "rm -rf는 훅에 의해 차단됩니다. 개별 파일 삭제를 사용하세요."
}
}'
exit 0
fi
exit 0
동작:
1. 훅이 stdin으로 JSON을 받는다 (tool_input.command 포함)
2. rm -rf 패턴이면 permissionDecision: "deny" + 사유를 stdout에 출력
3. Claude Code가 도구 호출을 차단
if필드로 pre-filter:{"if": "Bash(rm *)"}를 넣으면 Bash 도구 중에서도rm *패턴만 훅을 호출한다. 불필요한 훅 실행 줄이기에 유용.
5.3 PostToolUse — 자동 포맷
편집 후 자동으로 포맷터 실행. Write 또는 Edit 도구가 성공했을 때.
.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-format.sh",
"timeout": 30
}
]
}
]
}
}
.claude/hooks/auto-format.sh:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx)
prettier --write "$FILE" 2>/dev/null || true
;;
*.py)
ruff format "$FILE" 2>/dev/null || true
;;
*.rs)
rustfmt "$FILE" 2>/dev/null || true
;;
esac
exit 0
포맷 실패해도(exit 0) 에이전트 루프는 계속된다. 만약 lint를 Claude에게 강제로 알려주고 싶으면 stdout에 JSON으로 decision: "block" + reason을 출력한다:
if ! prettier --check "$FILE" > /tmp/lint.out 2>&1; then
jq -n --arg reason "$(cat /tmp/lint.out)" '{
decision: "block",
reason: $reason,
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: "Lint 실패. 수정 후 재시도 필요."
}
}'
exit 0
fi
5.4 PreCompact — 핸드오프 문서 자동 생성
/compact 직전에 handoff 문서를 남겨두면 세션 사이 연속성을 확보할 수 있다.
.claude/settings.json:
{
"hooks": {
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-compact.sh"
}
]
}
]
}
}
.claude/hooks/pre-compact.sh:
#!/bin/bash
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
HANDOFF_DIR="$CLAUDE_PROJECT_DIR/tasks/handoffs"
mkdir -p "$HANDOFF_DIR"
cat > "$HANDOFF_DIR/handoff-$TIMESTAMP.md" <<EOF
## Progress
- (compact 직전 진행 상황을 이어서 작성)
## Decisions
- (중요한 결정을 명시)
## Next Steps
- (다음 세션에서 이어갈 것)
## Blockers
- (막혀있는 이슈)
EOF
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreCompact",
"additionalContext": "Compact 전 $HANDOFF_DIR/handoff-$TIMESTAMP.md를 채워주세요."
}
}
EOF
exit 0
Tip: 메모리 시스템을 구축한 프로젝트라면 세션별 스냅샷(
tasks/sessions/)을 여기서 작성하면/compact후에도 세션 복원 가능.
6. 입력 JSON — 훅이 받는 것
모든 훅은 stdin으로 JSON을 받는다. 공통 필드:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/dir",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"agent_id": null,
"agent_type": null
}
이벤트별 추가 필드:
- PreToolUse / PostToolUse: tool_name, tool_input (전체 인자)
- SessionStart: source (startup/resume/clear/compact), model
- UserPromptSubmit: prompt (사용자가 입력한 문자열)
- PreCompact: trigger (manual/auto)
각 도구의 tool_input 스키마는 공식 /hooks 페이지 중반에 자세히 있다 (Bash/Write/Edit/Read/Glob/Grep/WebFetch/WebSearch 등).
7. 출력 JSON — 훅이 돌려주는 것
stdout에 JSON을 쓰면 Claude Code가 해석한다. 훅이 아무 출력도 하지 않으면 그냥 통과.
7.1 범용 필드
{
"continue": true,
"stopReason": "메시지",
"suppressOutput": false,
"systemMessage": "사용자에게 보여줄 경고"
}
continue: false면 Claude 자체를 정지.suppressOutput: true면 stdout이 디버그 로그에 기록되지 않음.
7.2 PreToolUse 전용 — 더 풍부한 제어
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "사용자에게 보여줄 사유",
"updatedInput": {
"command": "npm run lint"
},
"additionalContext": "Claude에게 추가로 줄 컨텍스트"
}
}
permissionDecision 4가지:
- allow — 권한 프롬프트 건너뛰고 허용
- deny — 도구 호출 차단
- ask — 사용자에게 확인 요청
- defer — 현재 턴 종료 후 나중에 이어서 (SDK 용)
우선순위: deny > defer > ask > allow.
updatedInput으로 명령을 수정해서 실행시킬 수도 있다. 예: git commit을 가로채서 git commit -s로 바꾸기.
8. Exit Code 함정 (가장 자주 실수하는 곳)
| Exit code | 동작 |
|---|---|
| 0 | stdout의 JSON을 파싱 (정상 경로) |
| 2 | 차단(block) — stderr를 에러 메시지로 |
| 기타 | 비차단 에러 — stderr를 transcript에 표시하고 계속 |
Unix 관례와 반대로 exit 1은 차단하지 않는다. 정책을 강제하려면 반드시 exit 2 또는 exit 0 + JSON decision: "block" 사용.
이벤트마다 exit 2의 의미가 다르다:
| 이벤트 | exit 2 동작 |
|---|---|
PreToolUse |
도구 호출 차단 |
UserPromptSubmit |
프롬프트 차단 + 지움 |
Stop/SubagentStop |
정지 막고 계속 |
PreCompact |
압축 차단 |
PostToolUse |
차단 불가 (Claude에게 메시지 전달) |
PermissionDenied/StopFailure |
무시 |
SessionStart/Notification |
사용자에게만 표시 |
차단이 안 되는 이벤트에서 exit 2를 써도 아무 효과가 없다. 공식 표로 반드시 확인.
9. 실전 — 훅 3개 세트업
지금까지 본 3개를 합친 최소 실용 세트. .claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh"}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-format.sh", "timeout": 30}
]
}
],
"PreCompact": [
{
"hooks": [
{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-compact.sh"}
]
}
]
}
}
디렉터리 구조:
.claude/
├── settings.json
└── hooks/
├── session-start.sh
├── block-rm.sh
├── auto-format.sh
└── pre-compact.sh
모든 스크립트에 chmod +x 부여.
10. 디버깅 — 훅이 안 뜰 때
10.1 /hooks 명령으로 확인
세션에서 /hooks를 실행하면 현재 로드된 훅 전체 목록이 뜬다. 내가 등록한 훅이 안 보이면 설정 파일 경로가 틀렸거나 JSON 파싱 에러.
10.2 --debug hooks 플래그
claude --debug "hooks"
훅 실행 로그가 실시간으로 찍힌다. 필터 예: --debug "api,hooks".
10.3 Claude에게 훅 짜달라고 요청
훅 스크립트는 Claude가 잘 쓰는 영역이다. 예:
Write a hook that runs eslint after every file edit.
Write a hook that blocks writes to the migrations/ folder.
그 뒤 .claude/settings.json을 직접 열어 /hooks로 검증.
10.4 deduplication
같은 command 문자열 / HTTP URL은 이벤트당 한 번만 실행된다. 같은 훅을 두 번 걸었는데 한 번만 실행되면 중복 제거 때문.
11. 보안 · 주의점
- Exit 1은 차단 안 됨 — 반드시 exit 2 또는 JSON
decision: "block"사용. - stdin JSON 파싱 실패는 훅의 exit code와 무관하게 Claude Code가 무시할 수 있음.
jq -r결과 항상 체크. - Timeout 기본값 — command 600s, prompt 30s, agent 60s. 긴 작업은
async: true로 백그라운드. - 관리자 설정의
allowManagedHooksOnly— managed settings로 사용자 훅 전체 무효화 가능. - HTTP 훅의
allowedEnvVars— 환경 변수 주입은 명시적으로 허용된 것만 interpolate. - MCP 서버 미연결 시 — MCP 도구 훅은 OAuth 흐름을 트리거하지 않고 그냥 실패.
12. 반대 시나리오 — 훅을 쓰지 말아야 할 때
- 판단이 필요한 일 →
prompt또는agent타입 훅을 쓰거나, 그냥 CLAUDE.md 규칙으로 두기. 결정이 규칙으로 정형화되지 않는다면 LLM이 판단하는 게 낫다. - 잦은 변경이 예상되는 규칙 →
settings.json을 자주 고치게 되고 팀 git 충돌 빈번. 이럴 땐 CLAUDE.md 쪽이 유연. - 플러그인·스킬로 충분한 경우 — 세션 시작 컨텍스트 주입은 skill로도 가능. 훅은 "exception 없이 반드시" 필요한 경우에만.
- 초보자 혼자 쓰는 개인 프로젝트 → 훅이 잘못 작동하면 모든 작업이 막힌다. 위험 차단 같은 소수 훅만 시작해서 점진 확장.
13. 다음 단계
훅이 익숙해졌다면:
- CLAUDE.md 작성 가이드 — 권고 레이어와의 분업.
- 슬래시 커맨드 총정리 —
/hooks명령 외 다른 디버깅 명령. - Claude Code 토큰·캐시 비용 완전 해부 — 훅으로 로깅해야 보이는 비용 구조 (예정).
참고
- 공식 Hooks 레퍼런스 — 25+ 이벤트 전체 스키마
- 공식 Hooks 가이드 — 시작 단계 튜토리얼
- 공식 Memory 문서
- 공식 Settings
이 글은 "AI 코딩 CLI 진입 가이드" 시리즈의 4/15 편입니다. last verified: 2026-04-25 (Claude Code 공식 Hooks 레퍼런스 기준).
댓글
댓글 쓰기