들어가며
지난 차시(27차시)에서 Subagent를 닫으며 한 줄이 남았다 — “다음 차시부터 Hooks를 본격적으로 다룬다.” 이번 차시가 그 시작이다.
27차시까지는 Skills와 Subagent로 Claude가 무엇을 하는가를 만들었다면, 섹션 6(Hooks)부터는 Claude가 일하는 동안 자동으로 무엇을 끼워 넣는가를 만든다. 파일을 편집한 직후 포맷터를 돌리는 일, rm -rf 명령이 나가기 전에 차단하는 일, 세션 시작 시 환경 변수를 주입하는 일 — 이런 부수 동작을 Claude가 매번 똑같이 한다고 믿는 대신 시스템에 못 박아 두는 게 Hooks다.
이번 차시는 본격적인 실전 패턴(29~31차시)에 들어가기 전 준비다. 다룰 것은 셋이다.
- Hook이 무엇이고 왜 쓰는가 — 시스템 프롬프트 지시와 어떻게 다른가
- 어떤 이벤트가 언제 발화하는가 — lifecycle 카탕스(session/turn/tool/async/agent)별 분류
- 어디에 어떻게 정의하는가 —
settings.json4단 우선순위, JSON 구조, matcher 규칙, exit code, 환경 변수
다음 차시부터는 이 이벤트들 중 가장 자주 쓰는 셋 — PostToolUse(29), PreToolUse(30), SessionStart/Stop(31) — 을 하나씩 깊이 본다.
Hook이란
핵심 정의다.
Hook = Claude Code의 정해진 lifecycle 시점에서 자동으로 실행되는 사용자 정의 액션
“사용자 정의 액션”은 셸 명령만이 아니다. 다음 다섯 가지 중 하나다 — 본문 뒤에 더 자세히 본다.
- 셸 명령 (
type: command) — 가장 흔함 - HTTP 엔드포인트 (
type: http) — 원격 서버로 POST - MCP 도구 (
type: mcp_tool) — 연결된 MCP 서버의 도구 호출 - LLM 프롬프트 (
type: prompt) — Claude에게 yes/no 평가 위임 - 서브에이전트 (
type: agent) — 검증을 위해 subagent 호출 (실험적)
발화 시점은 settings.json에 정의된 이벤트 이름으로 지정한다. 예시:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier --write \"$CLAUDE_FILE_PATH\""
}
]
}
]
}
}
→ Edit 또는 Write 도구가 실행된 직후 prettier를 자동으로 돌린다. Claude에게 “포맷터 돌려줘”라고 부탁하지 않아도 시스템이 보장한다.
Hook vs CLAUDE.md vs 시스템 프롬프트
같은 일을 시킬 수 있는 경로가 셋 있는데 성격이 다르다.
| 동작 시점 | 신뢰성 | |
|---|---|---|
| CLAUDE.md / 시스템 프롬프트 | Claude가 따르기로 결정했을 때 | 모델 판단에 의존 |
권한 시스템 (permissions.allow/deny) | 도구 호출 시 정적 규칙 | 매우 높음 |
| Hook | lifecycle 이벤트 발화 시 | 시스템 차원 보장 |
“편집 후 prettier를 돌려”를 CLAUDE.md에 적으면 모델이 잊을 수 있다. Hook은 반드시 돈다. 강한 보장이 필요하면 hook, 가이드라인이면 CLAUDE.md.
Hook이 빛나는 시나리오
- 자동 포맷팅/린트 — Edit/Write 직후
prettier,eslint --fix - 보안 가드 — Bash 호출 전
rm -rf,DROP TABLE패턴 차단 - 컨텍스트 주입 — 세션 시작 시
git status, 환경 변수를 자동 추가 - 로깅/감사 — 모든 도구 호출을 외부 시스템에 기록
- 의존성 자동 설치 — SessionStart에서
npm install - 알림 — 작업 완료 시 Slack/Mac 알림
- 컴팩션 직전 백업 —
PreCompact에서 transcript 백업
이벤트 분류 — 다섯 가지 카당스
Hook 이벤트는 언제 발화하느냐로 다섯 그룹이다. 이름만 외우는 것보다 그룹으로 묶어 두면 어떤 이벤트가 있는지 감이 잡힌다.
1. Session 단위 (세션당 1회)
| 이벤트 | 발화 시점 |
|---|---|
SessionStart | 세션 시작/재개. matcher로 startup/resume/clear/compact 구분 |
SessionEnd | 세션 종료. matcher로 clear/logout/prompt_input_exit 구분 |
Setup | --init-only나 -p --init/--maintenance 같은 일회성 트리거 |
용도 — 환경 변수 주입, 의존성 설치, 컨텍스트 사전 로딩.
2. Turn 단위 (사용자 요청마다 1회)
| 이벤트 | 발화 시점 |
|---|---|
UserPromptSubmit | 사용자가 메시지 제출, Claude가 처리하기 전 |
UserPromptExpansion | 슬래시 명령이 확장될 때 (matcher는 명령 이름) |
Stop | Claude가 응답을 마쳤을 때 |
StopFailure | API 에러로 턴이 끝났을 때 (블록 불가, 로깅용) |
용도 — 프롬프트 차단/증강, 작업 완료 알림, 추가 작업 강제.
3. 도구 호출 단위 (도구 호출마다)
| 이벤트 | 발화 시점 |
|---|---|
PreToolUse | 도구 실행 전. 허용/거부/사용자에게 묻기/입력 수정 가능 |
PostToolUse | 도구가 성공한 후 |
PostToolUseFailure | 도구가 실패한 후 (입력에 tool_error 포함) |
PermissionRequest | 권한 다이얼로그가 뜨려고 할 때 |
PermissionDenied | 사용자가 권한을 거부했을 때 |
PostToolBatch | 병렬 도구 배치가 완료된 후, 다음 모델 호출 직전 |
용도 — 위험 명령 차단, 자동 포맷팅, 도구 입력 sanitize, 결과 후처리.
4. 비동기/외부 이벤트
| 이벤트 | 발화 시점 |
|---|---|
Notification | 알림 발송 시 (권한 prompt, 인증 등). matcher로 종류 구분 |
ConfigChange | 설정 파일이 바뀌었을 때 |
CwdChanged | 작업 디렉토리가 바뀌었을 때 |
FileChanged | 감시 중인 파일이 바뀌었을 때 (matcher에 파일 이름 명시) |
WorktreeCreate / WorktreeRemove | git worktree 생성/제거 시 |
InstructionsLoaded | CLAUDE.md나 .claude/rules/ 파일이 로드될 때 |
PreCompact / PostCompact | 컨텍스트 압축 직전/직후 |
용도 — 외부 변경 감지, transcript 백업, 환경 동기화.
5. 에이전트/Task
| 이벤트 | 발화 시점 |
|---|---|
SubagentStart / SubagentStop | 서브에이전트 시작/종료 (matcher로 agent type 구분) |
TeammateIdle | Agent team 동료가 idle 상태로 갈 때 |
TaskCreated / TaskCompleted | Task 생성/완료 시 (exit code 2로 차단 가능) |
Elicitation / ElicitationResult | MCP 서버의 사용자 입력 요청 |
27차시에서 본 subagent 구성에서
hooksfrontmatter로PreToolUse/PostToolUse를 걸 수 있다고 했는데, 메인 세션의Stop은 서브에이전트 안에서는 자동으로SubagentStop으로 변환된다 — agent 내 정의 시 직접SubagentStop을 쓰지 않아도 동작한다.
설정 파일 — 어디에 정의하나
Hook은 settings.json의 hooks 키 아래에 정의한다. settings 파일은 네 단(+ managed)으로 쌓이고, 같은 키가 충돌하면 우선순위가 높은 쪽이 이긴다.
| 우선순위 | 위치 | 범위 | git 공유 |
|---|---|---|---|
| 1 (최고) | Managed (조직 정책) | 모든 사용자 | IT가 배포 |
| 2 | CLI flag (--settings) | 현재 세션 | — |
| 3 | .claude/settings.local.json | 프로젝트, 본인만 | 자동 gitignore |
| 4 | .claude/settings.json | 프로젝트, 팀 공유 | 커밋됨 |
| 5 (최저) | ~/.claude/settings.json | 본인의 모든 프로젝트 | — |
권한 규칙(
permissions.allow/deny)은 merge되는데, hooks도 마찬가지로 여러 스코프에서 정의된 게 모두 발화한다. 같은 이벤트에 hook을 user와 project 양쪽에서 걸면 둘 다 돈다.
기본 JSON 구조
세 단계로 중첩된다 — 이벤트, matcher 그룹, 핸들러 리스트.
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "/path/to/handler.sh",
"timeout": 600
},
{
"type": "command",
"command": "another-handler"
}
]
}
]
}
}
읽는 법 — hooks.EventName은 matcher 그룹의 배열이고, 각 그룹의 hooks는 핸들러의 배열이다. matcher가 매치되면 그 그룹 안의 핸들러가 모두 순서대로 발화한다.
Matcher 패턴 규칙
matcher는 hook이 발화할지 말지를 거른다. 규칙은 세 갈래다.
| 패턴 형태 | 평가 방식 | 예시 |
|---|---|---|
"*", "", 생략 | 모두 매치 | 항상 발화 |
알파벳/숫자/_/|만 | 정확 매치 또는 파이프 리스트 | Bash, Edit|Write |
| 그 외 문자 포함 | JavaScript 정규식 | ^Notebook, mcp__memory__.* |
이벤트별 matcher 의미
같은 matcher 문자열이 이벤트마다 다른 것을 거른다.
| 이벤트 | matcher가 거르는 것 |
|---|---|
PreToolUse/PostToolUse 등 도구 이벤트 | 도구 이름 (예: Bash, mcp__memory__.*) |
SessionStart | 시작 소스 (startup, resume, clear, compact) |
Setup | CLI 트리거 (init, maintenance) |
SessionEnd | 종료 이유 (clear, logout, prompt_input_exit) |
Notification | 알림 종류 (permission_prompt, auth_success) |
SubagentStart/SubagentStop | agent 이름 (Explore, general-purpose, 커스텀) |
ConfigChange | 설정 소스 (user_settings, policy_settings) |
FileChanged | 리터럴 파일 이름 (.envrc|.env) |
CwdChanged, UserPromptSubmit, PostToolBatch 등 | matcher 미지원 — 항상 발화 |
MCP 도구 매칭
MCP 도구는 mcp__<서버>__<도구> 패턴을 따른다. 정규식으로 묶으면 한 서버의 모든 도구를 잡을 수 있다.
{ "matcher": "mcp__memory__.*" }
if 필드 — 권한 규칙 문법
도구 이벤트에서 추가로 좁히고 싶을 때 if 필드를 쓴다 — 권한 규칙과 같은 문법이다.
{
"type": "command",
"if": "Bash(rm *)",
"command": "./block-rm.sh"
}
→ Bash 도구의 rm 명령 호출에만 발화. matcher로 도구 종류를 거르고, if로 인수 패턴까지 좁힌다.
Hook 타입 — 다섯 가지
1. command — 셸 명령 (가장 흔함)
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh",
"timeout": 600
}
stdin으로 JSON 입력을 받고, stdout으로 JSON 출력(또는 메시지)을 낸다.
Exec form vs Shell form — args를 같이 지정하면 셸을 거치지 않고 직접 실행 (공백 있는 경로 안전).
{
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/format.js", "--fix"]
}
args 없이 한 줄 문자열이면 셸이 토큰화 (파이프/글롭/&& 가능).
2. http — HTTP POST
{
"type": "http",
"url": "https://hooks.example.com/claude",
"timeout": 30,
"headers": { "Authorization": "Bearer $MY_TOKEN" },
"allowedEnvVars": ["MY_TOKEN"]
}
원격 서버에 JSON을 POST한다. 2xx 응답에 결정 JSON을 담아 보내면 블로킹 효과 가능.
보안 가드: managed settings의
allowedHttpHookUrls패턴에 매치하는 URL만 허용 가능,httpHookAllowedEnvVars로 인터폴레이션 가능한 env var 제한.
3. mcp_tool — MCP 서버 도구 호출
{
"type": "mcp_tool",
"server": "security_scanner",
"tool": "scan",
"input": { "file_path": "${tool_input.file_path}" }
}
연결된 MCP 서버의 도구를 hook에서 직접 호출한다. 32차시 이후의 MCP와 묶이는 패턴.
4. prompt — Claude에게 평가 위임
{
"type": "prompt",
"prompt": "Is this command safe? $ARGUMENTS",
"model": "fast-model",
"timeout": 30
}
Claude에게 yes/no 평가를 시키고 그 결과로 hook 결정을 내린다. 패턴이 너무 복잡해 정적 규칙으로 거르기 어려울 때 쓴다 (비용 듦).
5. agent — 서브에이전트 검증 (실험적)
{
"type": "agent",
"prompt": "Verify the deployment checklist. $ARGUMENTS"
}
서브에이전트를 띄워 검증한다. 무거운 검증이 필요할 때.
Hook 입출력 — 표준 인터페이스
입력 (stdin JSON)
모든 이벤트가 공통으로 받는 필드 (이벤트마다 추가 필드가 더 있음):
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"permission_mode": "default",
"hook_event_name": "PostToolUse",
"effort": { "level": "high" },
"agent_id": "subagent-uuid",
"agent_type": "agent-name"
}
도구 이벤트에는 tool_name과 tool_input(전체 입력)이 추가로 들어온다 — tool_input.command(Bash), tool_input.file_path(Edit) 같은 식으로 jq로 빼서 쓴다.
환경 변수
스크립트가 받는 주요 환경 변수:
| 변수 | 설명 |
|---|---|
$CLAUDE_PROJECT_DIR | 프로젝트 루트 |
$CLAUDE_PLUGIN_ROOT | 플러그인 설치 경로 |
$CLAUDE_PLUGIN_DATA | 플러그인 영구 데이터 디렉토리 |
$CLAUDE_EFFORT | 현재 effort 레벨 |
$CLAUDE_ENV_FILE | env var 파일 (SessionStart/Setup/CwdChanged/FileChanged에서만) |
입력 JSON의
tool_input.file_path도 셸 환경 변수로 풀어주는 게 과거 동작이었다. 현재는 stdin JSON에서jq -r '.tool_input.file_path'로 빼는 게 권장 방식 —$CLAUDE_FILE_PATH같은 이름의 환경 변수가 자동으로 채워진다는 보장은 없다.
Exit code의 의미
| Exit code | 의미 |
|---|---|
| 0 | 성공. stdout이 JSON이면 파싱되어 처리됨 |
| 2 | 블로킹 에러. stderr가 사용자/Claude에게 표시됨. 이벤트별로 효과 다름 (예: PreToolUse면 도구 실행 차단) |
| 그 외 | 비블로킹 에러. stderr는 transcript 첫 줄에, 전체는 디버그 로그에 |
흔한 함정 — 블로킹하려면 exit 1이 아니라 exit 2다. exit 1이나 다른 값은 에러 보고만 하고 작업은 그대로 진행된다.
JSON 출력
stdout JSON으로 더 정교한 제어가 된다 (exit 0 + JSON, exit 2면 JSON 무시).
모든 이벤트 공통 필드:
{
"continue": true,
"stopReason": "messages",
"suppressOutput": false,
"systemMessage": "warning"
}
컨텍스트 추가 — Claude에게 추가 정보 전달:
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Current branch: main"
}
}
결정 제어 — PreToolUse 같은 이벤트에서 허용/거부:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask|defer",
"permissionDecisionReason": "Reason",
"updatedInput": { "modified": "tool_input" }
}
}
출력은 10KB로 캡 — 넘으면 파일로 저장된다. additionalContext는 명령형이 아니라 사실로 적는 게 안전 (prompt injection 방어 작동).
작은 예제 — 두 가지 형태
A. 한 줄 명령 (Edit 후 prettier)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier --write \"$(jq -r '.tool_input.file_path')\""
}
]
}
]
}
}
B. 스크립트 + JSON 결정 (Bash rm -rf 차단)
.claude/hooks/block-rm.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE '\brm\s+-rf\b'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "rm -rf blocked by safety hook"
}
}'
exit 0
fi
exit 0
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
chmod +x도 잊지 말 것.
자세한 코드와 변형은 30차시(PreToolUse와 보안)에서 더 깊이 본다 — 이번 차시는 구조만.
/hooks 명령으로 등록 확인
세션 내에서 /hooks를 입력하면 현재 활성 hook이 카탈로그로 뜬다.
> /hooks
표시되는 정보:
- 이벤트 이름과 카운트
- 각 matcher와 핸들러 설정
- 정의된 소스 파일 경로
- 읽기 전용 (수정은 JSON 파일에서 직접)
여러 스코프에서 정의된 게 다 합쳐져 보이므로, 같은 이벤트에 user/project/local 어디서 뭐가 걸려 있는지 확인 가능. 디버깅의 1순위 도구.
일괄 비활성화
전체 hook을 끄려면:
{
"disableAllHooks": true
}
CLI에서 임시로:
claude --debug
→ 디버그 모드는 hook 실행을 비활성화하지 않지만 자세한 로그를 보여주어 무엇이 발화했는지 추적 가능.
Managed settings의 hook은 사용자/프로젝트
disableAllHooks로 꺼지지 않는다. 조직 정책이 우선.
Skills/Agents의 frontmatter hook
27차시에서 본 subagent frontmatter도 hooks를 받는다 — 컴포넌트가 활성화된 동안만 동작.
---
name: secure-ops
description: ...
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/security-check.sh"
---
→ 이 subagent가 위임받았을 때에만 hook이 걸린다. settings.json에 적는 것과 같은 JSON 구조지만 스코프가 컴포넌트 단위다. Skills frontmatter도 마찬가지.
Stophook을 subagent frontmatter에 적으면 자동으로SubagentStop으로 변환된다.
흔한 함정
- 블로킹은 exit 2 — exit 1은 비블로킹 에러
- JSON 출력은 exit 0에서만 — exit 2면 JSON 무시되고 stderr만 처리됨
- matcher 정규식 vs 정확 매치 — 알파벳/숫자/
_/|만 있으면 정확 매치, 그 외면 정규식.Bash.같은 점 하나로 동작이 바뀜 - 공백 있는 경로 — exec form(
args배열) 쓰거나 셸에서 명시적 쿼팅.prettier --write $CLAUDE_FILE_PATH처럼 따옴표 없이 쓰면 공백에서 깨짐 $CLAUDE_ENV_FILE는 일부 이벤트만 — SessionStart/Setup/CwdChanged/FileChanged 외에는 없음additionalContext를 명령형으로 — “do X”가 아니라 “X is the case”로 적어야 prompt injection 방어가 통과시킴- 여러 스코프의 hook 중복 — user와 project에 같은 matcher 두면 양쪽 다 발화 (의도된 동작)
/hooks로 확인 — 적용 안 되는 것 같으면 일단/hooks메뉴로 등록 여부 확인
정리
핵심 요점
- Hook = 정해진 lifecycle 시점에 자동 실행되는 사용자 액션 — CLAUDE.md 가이드보다 시스템 보장 강함
- 다섯 타입 —
command(셸),http(POST),mcp_tool(MCP),prompt(Claude 평가),agent(서브에이전트, 실험) - 다섯 카당스 — session 단위 / turn 단위 / tool 호출 단위 / 비동기/외부 / 에이전트·task
- 자주 쓰는 이벤트 —
PreToolUse(보안 게이트),PostToolUse(포맷팅/검사),SessionStart(환경 설정),Stop(완료 후속 작업),UserPromptSubmit(프롬프트 증강) - Settings 4단 + Managed —
~/.claude/settings.json<.claude/settings.json<.claude/settings.local.json< CLI < Managed - JSON 구조 3단 —
hooks.이벤트→matcher그룹 배열 →hooks핸들러 배열 - Matcher —
"*"/생략은 전부, 알파벳/숫자/_/|만이면 정확 매치, 그 외는 정규식 - 이벤트별 matcher 의미가 다름 — 도구 이벤트는 도구 이름, SessionStart는 시작 소스, Notification은 알림 종류 등
if필드로 더 좁힘 — 권한 규칙 문법(Bash(rm *),Edit(*.ts))- Exit code — 0 정상, 2가 블로킹, 그 외 비블로킹 에러
- JSON 출력으로 정교한 제어 —
hookSpecificOutput.permissionDecision로 허용/거부/묻기/연기 - 환경 변수 —
$CLAUDE_PROJECT_DIR,$CLAUDE_PLUGIN_ROOT,$CLAUDE_ENV_FILE(특정 이벤트) /hooks메뉴 — 어디 스코프에서 무엇이 걸려 있는지 확인. 디버깅의 1순위disableAllHooks— 일괄 끄기 (managed hook은 안 꺼짐)- Skill/Subagent frontmatter — 같은 JSON 구조로 컴포넌트 스코프 hook 가능
다음 단계
다음 세 차시에서 가장 자주 쓰는 이벤트를 하나씩 깊이 본다.
- 29차시:
PostToolUseHook 활용 — Edit/Write 후 prettier/eslint, 테스트 자동 실행, 매처를 어떻게 좁힐 것인가 - 30차시:
PreToolUseHook과 보안 — 위험 명령 차단, 검증 스크립트 작성, JSON 결정 응답으로 사용자에게 묻기 - 31차시:
SessionStart와StopHook — 의존성 자동 설치, 환경 변수 주입, 종료 시 정리 작업, 원격 세션 분기