Claude Code 훅 완전 해설: 에이전트를 감싸는 결정론적 계층
Claude Code 훅이란 무엇입니까? 훅은 Claude Code가 라이프사이클의 정해진 지점에서 자동으로 실행하는 사용자 정의 셸 명령입니다(HTTP 엔드포인트, MCP 도구, 모델 프롬프트도 포함합니다). 도구 호출 직전, 편집 직후, 세션 시작 시점, Claude가 응답을 마쳤을 때가 그런 지점입니다.1 CLAUDE.md가 모델이 아마도 따를 지시를 준다면, 훅은 모델이 협조하든 말든 실행됩니다. 세션 안에서 /hooks를 입력하면 모든 라이프사이클 이벤트와 거기에 무엇이 연결돼 있는지 볼 수 있습니다.
대부분의 개발자는 두 개의 제어 계층으로 Claude Code를 돌립니다. 에이전트가 무엇을 해도 되는지 통제하는 권한, 그리고 무엇을 해야 하는지 서술하는 CLAUDE.md입니다. 훅은 세 번째 계층이자, 무언가를 보장하는 유일한 계층입니다. 아래에서는 멘탈 모델, 현재 문서에 실린 모든 라이프사이클 이벤트, 정확한 입출력 계약, 설정 방법, 실제로 동작하는 다섯 가지 패턴, 그리고 선택 기준을 다룹니다. 모든 API 세부 사항은 2026년 8월 8일 기준 공식 훅 레퍼런스와 가이드에 대조해 확인했습니다. 이 시스템은 빠르게 바뀌니, 이 글과 레퍼런스가 어긋난다면 레퍼런스가 맞습니다. (Claude Code가 처음이십니까? 5분 설정이나 Claude Code 입문 경로부터 시작하세요.)
TL;DR: 훅은 stdin으로 JSON을 받고 종료 코드나 stdout의 JSON으로 답합니다. exit 0은 허용, exit 2는 차단(차단이 가능한 이벤트에서), 그리고 유닉스에서 관례적인 실패 코드인 exit 1은 아무것도 차단하지 않습니다. 이게 훅에서 가장 큰 함정입니다.2 설정은 settings.json의 PreToolUse, Stop 같은 이벤트 이름 아래에 두고 matcher로 거릅니다. 반드시 일어나야 하는 일에는 훅을, 모델이 알기만 하면 되는 일에는 CLAUDE.md를 쓰세요.
멘탈 모델: 비결정론적 핵심을 감싸는 보장
코딩 에이전트는 확률적인 시스템입니다. 편집할 때마다 Prettier를 실행하라고 하면 대개는 실행합니다. 하지만 변경이 사소해 보일 때, 컨텍스트가 길어질 때, 표현이 다르게 받아들여질 때는 그 단계를 건너뛸 수도 있습니다. CLAUDE.md도, 스킬도, 프롬프트도 모두 제안입니다. 품질은 높고 대개는 지켜지지만, 보장되지는 않습니다.
훅은 그 핵심을 감싸는 결정론적인 껍데기입니다. 가이드는 “훅은 사용자 정의 셸 명령입니다”라는 한 줄 정의로 시작해 요점을 분명히 말합니다. 훅은 “결정론적 제어, 즉 LLM이 실행하기로 선택하기를 기대하는 대신 특정 동작이 항상 일어나게” 해준다는 것입니다.3 (가이드의 그 한 줄은 지금의 실제 범위를 너무 축소해 말합니다. 레퍼런스의 더 완전한 정의에는 이미 HTTP 엔드포인트와 LLM 프롬프트가 들어가 있고, 핸들러는 MCP 도구로도 제공됩니다. 아래 설정 절에서 다룹니다.) 포매터는 편집할 때마다 돕니다. 명령 가드는 모든 Bash 호출을 평가합니다. 완료 게이트는 매번 종료를 검사합니다.
강제력은 겉치레가 아니라 진짜입니다. PreToolUse 훅은 권한 모드 검사보다 먼저 발동하므로, permissionDecision: "deny"를 반환하는 훅은 bypassPermissions 모드에서도, --dangerously-skip-permissions 아래에서도 도구를 차단합니다. 반대 방향은 성립하지 않습니다. "allow"를 반환하는 훅이 설정의 deny 규칙을 느슨하게 만들 수는 없기 때문입니다. 훅은 권한이 허용하는 것보다 정책을 더 조일 수는 있어도, 약하게 할 수는 없습니다.4
라이프사이클: 모든 훅 이벤트
2026년 8월 8일 기준으로 레퍼런스는 31개의 훅 이벤트를 문서화하고 있습니다.1 이들은 세 가지 주기로 나뉩니다. 세션당 한 번(SessionStart, SessionEnd), 턴당 한 번(UserPromptSubmit, Stop, StopFailure), 그리고 에이전트 루프 안의 모든 도구 호출마다(PreToolUse, PostToolUse)입니다. 나머지는 설정 변경, 압축, 서브에이전트, MCP 상호작용 같은 특정 조건에서 발동합니다.
| 이벤트 | 발동 시점 | 실제 활용 예 |
|---|---|---|
SessionStart |
세션이 시작되거나 재개될 때 | git 브랜치와 열린 이슈를 컨텍스트로 주입합니다 |
Setup |
--init-only, 또는 -p 모드의 --init/--maintenance |
에이전트 실행 전에 CI에서 의존성을 설치합니다 |
UserPromptSubmit |
프롬프트를 제출한 뒤, Claude가 처리하기 전 | 현재 날짜를 덧붙이고, 시크릿이 든 프롬프트를 거부합니다 |
UserPromptExpansion |
입력한 명령이 프롬프트로 확장될 때 | 스킬이나 명령의 확장을 감사하거나 거부합니다 |
PreToolUse |
도구 호출이 실행되기 전 | 파괴적인 셸 명령을 차단합니다 |
PermissionRequest |
권한 대화상자가 뜰 때 | 신뢰하는 명령을 자동 승인해 확인 절차를 건너뜁니다 |
PermissionDenied |
자동 모드 분류기가 도구 호출을 거부할 때 | retry: true를 반환해 모델이 재시도하게 합니다 |
PostToolUse |
도구 호출이 성공한 뒤 | 편집된 파일을 모두 자동으로 포매팅합니다 |
PostToolUseFailure |
도구 호출이 실패한 뒤 | 실패한 명령을 분류용으로 기록합니다 |
PostToolBatch |
병렬 도구 호출 묶음 이후, 다음 모델 호출 전 | 에이전트 루프를 체크포인트하거나 중단합니다 |
Notification |
Claude Code가 알림을 보낼 때 | Claude가 입력을 기다릴 때 데스크톱 알림을 띄웁니다 |
MessageDisplay |
어시스턴트 메시지 본문이 표시되는 동안 | 화면에서 가립니다(표시 전용이라 트랜스크립트는 그대로입니다) |
SubagentStart |
서브에이전트가 생성될 때 | 에이전트 유형별 컨텍스트를 주입합니다 |
SubagentStop |
서브에이전트가 끝날 때 | 결과가 돌아가기 전에 서브에이전트 출력을 검증합니다 |
TaskCreated |
TaskCreate로 작업이 생성될 때 |
작업 이름이나 범위 규칙을 강제합니다 |
TaskCompleted |
작업이 완료로 표시될 때 | 완료가 확정되기 전에 인수 조건을 검증합니다 |
Stop |
Claude가 응답을 마칠 때 | 완료 게이트. 테스트가 통과할 때까지 종료를 막습니다 |
StopFailure |
API 오류로 턴이 끝날 때 | rate_limit이나 billing_error를 알립니다(로그 전용이라 출력은 무시됩니다) |
TeammateIdle |
에이전트 팀의 동료가 유휴 상태로 들어가기 직전 | 큐를 돌려 동료들이 계속 일하게 합니다 |
InstructionsLoaded |
CLAUDE.md나 .claude/rules/*.md 파일이 컨텍스트로 로드될 때 |
어떤 지시가 세션에 들어왔는지 기록합니다 |
ConfigChange |
세션 도중 설정 파일이 바뀔 때 | 허가되지 않은 설정 수정을 차단합니다 |
CwdChanged |
작업 디렉터리가 바뀔 때 | direnv 방식의 환경을 다시 불러옵니다 |
DirectoryAdded |
/add-dir나 SDK의 register_repo_root로 세션 도중 작업 디렉터리가 등록될 때(v2.1.219 이상) |
그 저장소의 컨텍스트를 합류하는 순간 불러옵니다 |
FileChanged |
감시 중인 파일이 디스크에서 바뀔 때 | .env가 바뀌면 환경 변수를 갱신합니다 |
WorktreeCreate |
--worktree나 isolation: "worktree"로 worktree가 생성될 때 |
기본 git worktree 프로비저닝을 대체합니다 |
WorktreeRemove |
worktree가 제거될 때 | 세션이나 서브에이전트 종료 시 맞춤 정리를 합니다 |
PreCompact |
컨텍스트 압축 전 | 잃으면 안 되는 상태를 저장합니다 |
PostCompact |
압축이 끝난 뒤 | 핵심 컨텍스트를 다시 주입합니다 |
Elicitation |
MCP 서버가 사용자 입력을 요청할 때 | 헤드리스 실행에서 폼을 자동으로 채웁니다 |
ElicitationResult |
MCP elicitation에 응답한 뒤 | 응답이 돌아가기 전에 검증하거나 덮어씁니다 |
SessionEnd |
세션이 종료될 때 | 로그를 보관하고 리소스를 정리합니다 |
이 대부분은 필요하지 않을 것입니다. 실제 운영 구성은 거의 전부 다섯 개로 만들어집니다. PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop입니다. 나머지는 필요해지는 그날을 위해 존재합니다.
계약: JSON을 받고, 종료 코드나 JSON을 내보냅니다
명령형 훅은 stdin으로 JSON을 받고 종료 코드, stdout, stderr로 답합니다. (HTTP 훅은 같은 JSON을 POST 본문으로 받고 응답 본문으로 답합니다.)2
모든 이벤트는 공통 봉투를 전달합니다. session_id, transcript_path, cwd, hook_event_name이 들어가고 대부분의 이벤트에는 permission_mode도 붙으며, 여기에 이벤트별 필드가 더해집니다. Bash 명령에 대한 PreToolUse 훅이 받는 내용은 이렇습니다.
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" }
}
다른 이벤트는 뒷부분이 달라집니다. UserPromptSubmit은 prompt를, SessionStart는 source(startup/resume/clear/compact/fork — 다섯 번째 값은 v2.1.214의 포크된 세션과 함께 들어왔고, 예전의 네 값짜리 목록에서 복사해 온 source 매칭 훅은 포크를 조용히 놓칩니다)를, Stop은 stop_hook_active와 last_assistant_message를 실어 보냅니다. 서브에이전트 안에서 발동한 훅은 여기에 더해 agent_id와 agent_type을 받습니다.2
종료 코드
결과는 세 가지입니다.2
- Exit 0 — 성공입니다. Claude Code는 stdout을 JSON 출력 필드로 파싱합니다. 대부분의 이벤트에서 stdout은 디버그 로그로만 가지만,
UserPromptSubmit,UserPromptExpansion,SessionStart에서는 평범한 stdout이 Claude가 볼 수 있는 컨텍스트로 추가됩니다. - Exit 2 — 차단 오류입니다. stdout은 JSON을 포함해 전부 무시되고, stderr가 오류 메시지로 Claude에게 전달됩니다. “차단”이 무슨 뜻인지는 이벤트마다 다릅니다.
- 그 밖의 종료 코드 — 차단하지 않는 오류입니다. 트랜스크립트에
<hook name> hook error알림이 뜨고 실행은 계속됩니다.
마지막 줄은 굵게 쓸 만합니다. exit 1은 아무것도 차단하지 않습니다. 문서도 이 점을 직접 경고합니다. 1이 유닉스에서 관례적인 실패 코드인데도, Claude Code는 exit 1을 차단하지 않는 오류로 처리하고 그대로 진행합니다. 정책을 강제하는 훅은 반드시 exit 2여야 합니다.2
이벤트별로 exit 2가 하는 일은 이렇습니다.2
| 이벤트 | exit 2의 효과 |
|---|---|
PreToolUse |
도구 호출을 차단합니다 |
PermissionRequest |
권한을 거부합니다 |
UserPromptSubmit |
처리를 차단하고 프롬프트를 지웁니다 |
UserPromptExpansion |
확장을 차단합니다 |
Stop / SubagentStop |
정지를 막고 대화를 계속 이어갑니다 |
TeammateIdle |
동료가 유휴 상태로 들어가는 것을 막습니다 |
TaskCreated / TaskCompleted |
생성을 롤백하거나 완료를 막습니다 |
ConfigChange |
설정 변경을 차단합니다(policy_settings는 예외입니다) |
PreCompact |
압축을 차단합니다 |
PostToolBatch |
다음 모델 호출 전에 에이전트 루프를 멈춥니다 |
Elicitation / ElicitationResult |
elicitation을 거부하거나 응답을 거절로 바꿉니다 |
WorktreeCreate |
0이 아닌 종료 코드는 모두 worktree 생성을 중단시킵니다 |
그 밖의 이벤트는 차단할 수 없습니다. PostToolUse와 PostToolUseFailure는 stderr를 Claude에게 보여줍니다(도구는 이미 실행됐기 때문입니다). SessionStart, Notification, SessionEnd, CwdChanged, FileChanged, PostCompact, SubagentStart, Setup은 stderr를 사용자에게만 보여주고, DirectoryAdded는 stderr를 디버그 로그로만 보냅니다. StopFailure, InstructionsLoaded, MessageDisplay, PermissionDenied는 종료 코드를 무시하는데, PermissionDenied에서 유일하게 통하는 지렛대는 JSON의 retry: true입니다.2
JSON 출력
차단이냐 침묵이냐보다 더 세밀하게 제어하려면 exit 0으로 끝내면서 stdout에 JSON 객체를 출력하세요. 먼저 규칙 하나부터 짚겠습니다. 종료 코드 아니면 JSON이지, 둘 다는 안 됩니다. JSON은 exit 0일 때만 처리되고, exit 2는 그것을 버립니다.5
공통 필드는 모든 이벤트에서 동작합니다. continue: false는 Claude를 완전히 멈추고(stopReason이 사용자에게 표시됩니다), suppressOutput은 stdout을 트랜스크립트에서 감추고, systemMessage는 사용자에게 경고를 보여주며, terminalSequence는 허용 목록에 있는 터미널 이스케이프(데스크톱 알림, 창 제목, 벨)를 내보냅니다. 판단 필드는 이벤트마다 다릅니다.5
| 이벤트 | 판단 방식 | 주요 필드 |
|---|---|---|
UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, ConfigChange, PreCompact |
최상위 decision |
decision: "block"과 reason(Claude에게 표시됩니다). 허용하려면 decision을 생략하세요 |
PreToolUse |
hookSpecificOutput |
permissionDecision: "allow" | "deny" | "ask" | "defer", 여기에 permissionDecisionReason과, 실행 전에 도구 인자를 다시 쓰는 updatedInput이 더해집니다 |
PermissionRequest |
hookSpecificOutput |
decision.behavior: "allow" | "deny", 선택적으로 decision.updatedInput |
PermissionDenied |
hookSpecificOutput |
retry: true는 모델에게 재시도해도 된다고 알립니다 |
PostToolUse |
hookSpecificOutput |
updatedToolOutput이 도구 결과를 대체합니다 |
Stop / SubagentStop |
hookSpecificOutput |
additionalContext. 훅 오류로 집계되지 않으면서 대화를 이어가는, 오류가 아닌 피드백입니다 |
SessionStart, Setup, SubagentStart |
컨텍스트 전용 | additionalContext, 그리고 SessionStart 전용인 initialUserMessage, sessionTitle, watchPaths, reloadSkills. 차단은 못 합니다 |
MessageDisplay |
hookSpecificOutput |
displayContent는 화면에 보이는 텍스트만 바꿉니다 |
Elicitation / ElicitationResult |
hookSpecificOutput |
action: "accept" | "decline" | "cancel", 그리고 content |
TeammateIdle, TaskCreated, TaskCompleted |
공통 continue |
continue: false와 stopReason이 동료나 작업 흐름을 통째로 멈춥니다(이벤트별 차단은 exit 2입니다) |
WorktreeCreate |
경로 반환 | 명령형 훅은 worktree 경로를 stdout에 출력하고, HTTP 훅은 hookSpecificOutput.worktreePath를 반환합니다. 실패하거나 경로가 없으면 생성이 실패합니다 |
WorktreeRemove, Notification, SessionEnd, PostCompact, InstructionsLoaded, StopFailure, CwdChanged, DirectoryAdded, FileChanged |
없음 | 부수 효과만 있습니다 |
사람들이 자주 걸리는 지점이 둘 있습니다. 첫째, PreToolUse는 최상위 decision 패턴의 예외입니다. 예전에는 최상위 decision/reason을 썼지만 이 이벤트에서는 더 이상 권장되지 않습니다("approve"/"block"은 각각 "allow"/"deny"에 대응합니다). hookSpecificOutput.permissionDecision을 쓰세요.5 둘째, 여러 PreToolUse 훅의 판단이 엇갈리면 우선순위는 deny > defer > ask > allow로, 가장 제한적인 답이 이깁니다. 그렇더라도 이 동점 처리에 기대기보다는 판단마다 담당 훅을 하나씩 정해 두세요.5
설정: settings.json, matcher, 범위
훅 설정은 세 단계로 중첩됩니다. 이벤트를 고르고, 언제 발동할지 거르는 matcher 그룹을 더하고, 실행할 훅 핸들러를 하나 이상 정의합니다.6
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "/path/to/lint-check.sh" }
]
}
]
}
}
이걸 어디에 두느냐가 범위를 결정합니다. ~/.claude/settings.json은 모든 프로젝트에 적용되고, .claude/settings.json은 프로젝트 범위이며 커밋할 수 있고, .claude/settings.local.json은 프로젝트 범위이면서 gitignore 대상입니다. 우선순위는 일반 설정과 같아서 관리 정책이 로컬보다, 로컬이 프로젝트보다, 프로젝트가 사용자보다 앞섭니다.9 훅은 플러그인(hooks/hooks.json)이나 스킬 또는 에이전트 프론트매터로도 배포할 수 있고, 기업 관리자는 사용자가 덮어쓸 수 없는 관리 훅을 강제할 수도 있습니다.6
matcher는 문자 구성에 따라 해석됩니다. "*", "", 또는 matcher를 생략하면 전부 매칭됩니다. 영문자, 숫자, _, -, 공백, 쉼표, |만으로 이뤄진 값은 정확한 문자열이나 목록입니다(Bash, Edit|Write). 그 밖의 값은 앵커 없는 JavaScript 정규식이 되므로 Edit.*는 Edit과 NotebookEdit 둘 다에 매칭됩니다. 도구 하나만 뜻할 때는 ^Edit$처럼 앵커를 붙이세요. matcher는 대소문자를 구분하고, 이벤트마다 자기 필드로 매칭합니다. 도구 이벤트는 도구 이름, SessionStart는 source, SubagentStart는 에이전트 유형, Notification은 알림 유형입니다.6 도구 이벤트를 더 날카롭게 거르고 싶다면 핸들러별 if 필드가 "Bash(git *)" 같은 권한 규칙 하나를 받습니다. 다만 최선 노력 방식이라(파싱할 수 없는 명령에서는 그냥 통과시킵니다) 확실한 보장이 필요하면 if가 아니라 권한 규칙을 쓰세요.6 알아 둘 의미 변화가 하나 있습니다. v2.1.214부터 if 안의 단일 세그먼트 경로 패턴(Edit(src/**) 같은 것)은 작업 디렉터리 바로 아래의 src에만 매칭됩니다. 그 릴리스 이전에 쓴 if는 packages/app/src/ 같은 중첩 경로에 조용히 매칭을 멈춥니다. 예전처럼 깊이에 상관없이 매칭하게 하려면 Edit(**/src/**)라고 쓰세요.6
핸들러는 다섯 종류입니다. command(셸), http(POST 엔드포인트), mcp_tool, prompt(단일 턴 모델 평가), agent(Read/Grep/Glob에 접근하는 서브에이전트, 실험적)입니다. 기본 타임아웃은 command/http/mcp_tool이 600초(UserPromptSubmit은 30초, MessageDisplay는 10초로 낮아집니다), prompt가 30초, agent가 60초이고, 훅마다 timeout으로 덮어쓸 수 있습니다.6 매칭된 훅은 모두 병렬로 실행되며 동일한 핸들러는 중복이 제거되고, $CLAUDE_PROJECT_DIR은 스크립트에서 프로젝트 루트를 가리킵니다.
확인은 /hooks로 하세요. 모든 이벤트와 거기 설정된 훅, 그리고 각각이 어느 설정 파일에서 왔는지 보여주는 읽기 전용 브라우저입니다. 무언가를 바꾸려면 JSON을 편집하세요(Claude에게 시켜도 됩니다). 잠시 전부 끄려면 "disableAllHooks": true를 설정하세요.6
다섯 가지 패턴
일반화했고 최소한으로 줄였습니다. 훅 튜토리얼은 이 가운데 몇 가지를 더 완전한 운영 버전으로 만들고, Apple 개발을 위한 훅은 이를 iOS 툴체인에 적용합니다.
1. 편집 후 자동 포매팅(PostToolUse)
공식 가이드에서 그대로 가져왔습니다. Claude가 건드린 파일은 예외 없이 포매팅됩니다.3
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}
스택에 맞춰 명령을 ruff format, gofmt, swiftformat으로 바꾸세요.
2. 위험한 명령 차단하기(PreToolUse, exit 2)
#!/bin/bash
# .claude/hooks/guard-bash.sh — register on PreToolUse, matcher "Bash"
command=$(jq -r '.tool_input.command // empty')
case "$command" in
*"rm -rf"* | *"git push --force"* | *"DROP TABLE"*)
echo "Blocked: matches a destructive pattern. Propose a safer alternative." >&2
exit 2 ;;
esac
exit 0
exit 2는 호출을 차단하고 stderr를 Claude에게 돌려주므로, Claude는 무작정 재시도하는 대신 방향을 바꿉니다. JSON으로 같은 일을 하려면 이유를 붙인 permissionDecision: "deny"인데, 이쪽은 "ask"(사람에게 넘기기)나 updatedInput(명령 다시 쓰기)으로 발전시킬 여지가 있습니다.5
3. 세션 시작 시 컨텍스트 주입하기(SessionStart)
SessionStart 훅의 평범한 stdout은 그대로 Claude가 볼 수 있는 컨텍스트가 됩니다. JSON은 필요 없습니다.1
#!/bin/bash
# .claude/hooks/session-context.sh — register on SessionStart
echo "Current branch: $(git branch --show-current)"
echo "Recent commits:"
git log --oneline -5
echo "Uncommitted files: $(git status --porcelain | wc -l | tr -d ' ')"
exit 0
이건 동적인 상태에 쓰세요. 정적인 관례는 CLAUDE.md에 두는 게 맞고, 문서 자체도 스크립트가 필요 없는 컨텍스트라면 CLAUDE.md를 권합니다.1
4. Stop에 거는 완료 게이트
Stop은 Claude가 응답을 마칠 때 발동합니다. 이걸 차단하면 조건이 충족될 때까지 에이전트가 계속 일하게 만들 수 있습니다.
#!/bin/bash
# .claude/hooks/stop-gate.sh — register on Stop
input=$(cat)
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # already continuing because of this hook; don't loop forever
fi
if ! npm test --silent > /tmp/stop-gate.log 2>&1; then
jq -n '{decision: "block", reason: "Tests are failing. Fix them before finishing. Log: /tmp/stop-gate.log"}'
fi
exit 0
stop_hook_active 확인이 중요합니다. Claude Code는 Stop 훅의 연속 차단을 기본 8회로 제한하는데(CLAUDE_CODE_STOP_HOOK_BLOCK_CAP으로 올릴 수 있습니다), 자기가 이미 계속 진행을 유발했는지 한 번도 확인하지 않는 게이트는 그 횟수를 단숨에 태워 버립니다.7 더 부드럽게 유도하려면 decision: "block" 대신 hookSpecificOutput.additionalContext를 반환하세요. 계속 진행되는 건 같지만, 훅 오류가 아니라 이름표가 붙은 피드백이 됩니다. 그리고 일회성 조건이라면 내장 /goal 명령이 설정 없이 쓰는 세션 범위의 프롬프트 기반 Stop 훅입니다.1
5. 디스패처: 하나의 입구, 여러 개의 작은 훅
훅 열 개를 등록한다는 건 기기와 프로젝트마다 어긋나는 settings.json 항목 열 개를 갖는다는 뜻입니다. 대안은 이벤트마다 디스패처를 하나 등록하고 관례로 라우팅하는 것입니다.
#!/bin/bash
# .claude/hooks/dispatch.sh — register once per event you care about
input=$(cat)
event=$(echo "$input" | jq -r '.hook_event_name')
dir="$CLAUDE_PROJECT_DIR/.claude/hooks/$event"
[ -d "$dir" ] || exit 0
for hook in "$dir"/*.sh; do
[ -x "$hook" ] || continue
echo "$input" | "$hook" || exit $?
done
exit 0
이제 가드를 추가하는 일은 .claude/hooks/PreToolUse/에 새 파일을 놓고 chmod +x 하는 게 전부입니다. settings.json은 그대로고, 각 스크립트는 따로 테스트할 만큼 작게 유지되며, 첫 exit 2가 그대로 전파됩니다. 한 가지 주의할 점이 있습니다. 디스패처는 Claude Code라면 병렬로 돌릴 것을 직렬화하고, 종료 코드 방식의 훅에 가장 잘 맞습니다. stdout에는 JSON 객체가 정확히 하나만 있어야 하니, JSON을 내보내는 훅은 단독으로 두는 게 좋습니다.5
훅과 CLAUDE.md, 스킬, 메모리
네 가지 메커니즘, 네 가지 역할입니다.
| 메커니즘 | 역할 | 고르는 기준 |
|---|---|---|
| 훅 | 강제 | 건너뛰는 게 불가능해야 한다면(포매팅, 안전, 게이트) 훅입니다 |
| CLAUDE.md | 안내 | 모델이 매 세션 알아야 할 관례라면(스택, 스타일, 명령) CLAUDE.md입니다 |
| 스킬 | 능력 | 자체 지시와 스크립트를 갖고 필요할 때 호출되는 절차라면 스킬입니다 |
| 메모리 | 기억 | 한 세션에서 배운 사실을 이후 세션들이 필요로 한다면 메모리입니다 |
실패는 양쪽 방향으로 일어납니다. 관례를 훅으로 새기면, 한 문장짜리 안내로 충분한 일을 강제하는 부서지기 쉬운 스크립트를 떠안게 됩니다. 정책을 CLAUDE.md 산문으로 적으면, 하필 중요한 날에 main으로 force push하는 에이전트를 떠안게 됩니다. 판단 기준은 이것입니다. 모델이 이걸 한 번 무시하면 대가가 무엇인가? 짜증으로 끝나면 CLAUDE.md, 사고가 되면 훅입니다.
훅이 할 수 없는 일
전부 공식 문서에서 가져온 정직한 한계입니다.7
- 훅은 도구나 슬래시 명령을 호출할 수 없습니다. 명령형 훅이 말할 수 있는 건 stdout, stderr, 종료 코드뿐입니다.
additionalContext로 돌려준 컨텍스트는 일반 텍스트로 주입됩니다. PostToolUse는 되돌릴 수 없습니다. 도구는 이미 실행됐습니다. 예방은PreToolUse의 몫입니다.Stop은 응답이 끝날 때마다 발동합니다. “작업 완료” 때만이 아니고, 사용자가 중단했을 때는 발동하지 않습니다(API 오류에서는 대신StopFailure가 발동합니다). 게이트 로직은 작업 도중의 정지도 견뎌야 합니다.PermissionRequest는 순수 헤드리스(-p) 실행에서는 발동하지 않습니다. Agent SDK의canUseTool콜백이 프롬프트를 제공하는-p실행과 백그라운드 서브에이전트의 도구 호출에서는 발동합니다. 그 밖의 자동화에는PreToolUse를 쓰세요.PreToolUse는@로 참조한 파일을 보지 못합니다. 프롬프트에서@로 끌어온 파일에는 도구 호출이 없습니다. 그 경로로부터 파일을 지키려면Readdeny 규칙을 쓰세요.1- 병렬 상황의
updatedInput은 설계상 믿을 수 없습니다. 여러 PreToolUse 훅이 같은 도구의 인자를 다시 쓰면 살아남는 건 하나뿐이고, 어느 것이 남을지는 고를 수 없습니다. 인자 재작성은 훅 하나에 맡기세요. - 타임아웃은 훅을 취소합니다. 명령형 훅의 기본값은 600초입니다(
UserPromptSubmit은 30초,MessageDisplay는 10초). 타임아웃되는 느린 게이트는 아예 실행되지 않은 게이트와 같습니다. - 출력은 10,000자에서 잘립니다. 넘치는 부분은 파일로 쓰이고 미리보기로 대체됩니다.
- 훅은 여러분의 사용자 권한을 그대로 갖고 실행됩니다. 레퍼런스 자신의 경고입니다. 훅은 “사용자 계정이 접근할 수 있는 모든 파일을 수정하거나 삭제하거나 접근할 수 있습니다. 설정에 추가하기 전에 모든 훅 명령을 검토하고 테스트하세요.”8 변수는 따옴표로 감싸고, 절대 경로를 쓰고, 민감한 파일은 건드리지 마세요.
- 망가진 훅은 고칠 때까지 모든 세션을 갉아먹습니다. 트랜스크립트 화면(
Ctrl+O),claude --debug-file /tmp/claude.log, 또는 세션 중의/debug로 디버깅하세요. 전형적인 함정은 시작할 때 뭔가를 출력하는 셸 프로필이 훅의 JSON 출력을 깨뜨리는 경우입니다.7
자주 묻는 질문
Claude Code 훅이란 무엇입니까?
훅은 Claude Code가 특정 라이프사이클 지점에서 자동으로 실행하는 사용자 정의 명령입니다. 셸 스크립트, HTTP 엔드포인트, MCP 도구, 모델 프롬프트가 여기에 해당합니다.3 stdin으로 이벤트 JSON을 받고 종료 코드나 JSON으로 답합니다. 도구 호출을 차단하고, 컨텍스트를 주입하고, 인자를 다시 쓰고, 에이전트를 계속 일하게 만듭니다. CLAUDE.md 지시와 달리 모델의 행동과 무관하게 매번 실행됩니다.
PreToolUse 훅과 권한은 무엇이 다릅니까?
권한 규칙은 선언적입니다. Claude Code가 직접 평가하는 정적인 allow/deny/ask 패턴입니다. PreToolUse 훅은 프로그래밍이 가능해서, 여러분의 코드가 도구 입력 전체를 살펴보고 판단합니다. 훅은 권한 모드 검사보다 먼저 발동하므로 훅의 "deny"는 bypassPermissions 모드에서도 유효합니다. 다만 훅의 "allow"가 설정의 deny 규칙을 덮어쓸 수는 없습니다.4 패턴으로 표현할 수 있는 것은 권한 규칙에 맡기고, 로직이나 외부 상태, 입력 재작성이 필요한 판단일 때 훅에 손을 뻗으세요.
헤드리스(-p) 모드에서도 훅이 동작합니까?
동작합니다. 다만 한 가지 미묘한 점이 있습니다. PermissionRequest 훅은 순수 -p 실행을 건너뜁니다(거기에는 권한 프롬프트를 제공하는 것이 없기 때문입니다). 물론 Agent SDK의 canUseTool 콜백이 프롬프트를 제공하는 경우와 백그라운드 서브에이전트의 도구 호출에서는 발동합니다. 순수 헤드리스 실행의 자동 권한 판단은 PreToolUse에 두세요.7 헤드리스 모드는 대화형 세션이 무시하는 선택지도 하나 열어 줍니다. permissionDecision: "defer"인데, 도구 호출을 잠시 멈춰서 이를 감싸는 프로세스(Agent SDK 앱이나 맞춤 UI)가 입력을 모아 나중에 세션을 재개할 수 있게 합니다.5
훅이 실행은 되는데 아무것도 차단하지 않는 이유는 무엇입니까?
거의 항상 계약 위반입니다. exit 1은 차단하지 않습니다. 차단하는 건 exit 2뿐이고, 그것도 차단을 지원하는 이벤트에서만입니다.2 JSON 판단은 exit 0일 때만 파싱되니, {"decision": "block"}을 출력하고 나서 exit 2로 끝나는 스크립트는 그 JSON을 버리게 됩니다. 그리고 matcher는 대소문자를 구분해서 bash는 절대 Bash에 매칭되지 않습니다. /hooks로 등록을 확인한 다음, 샘플 JSON을 스크립트에 파이프로 넣고 echo $?를 확인해 보세요.7
출처
2026년 8월 8일 기준 공식 문서에 대조해 확인했습니다. 훅 API는 Claude Code v2.1.x 릴리스들을 거치며 실질적으로 바뀌어 왔으니(새 이벤트, 새 필드, matcher 의미), 버전에 민감한 세부 사항은 “이 날짜 기준”으로 받아들이세요.
이 사이트의 관련 글: 프롬프트 훅과 에이전트 훅까지 포함한 시스템 전체 관점은 Claude Code 가이드의 훅 절, 완전한 설정이 딸린 다섯 개의 운영 빌드는 훅 튜토리얼, iOS에 적용한 패턴은 Apple 개발을 위한 훅, 아직 Claude Code를 설치하지 않았다면 퀵스타트를 보세요.
-
Anthropic, “Hooks reference — Hook lifecycle and hook events.” code.claude.com/docs/en/hooks#hook-events ↩↩↩↩↩↩
-
Anthropic, “Hooks reference — Hook input and output; exit code output; exit code 2 behavior per event.” code.claude.com/docs/en/hooks#exit-code-output ↩↩↩↩↩↩↩↩
-
Anthropic, “Automate actions with hooks.” code.claude.com/docs/en/hooks-guide ↩↩↩
-
Anthropic, “Hooks guide — Hooks and permission modes.” code.claude.com/docs/en/hooks-guide#hooks-and-permission-modes ↩↩
-
Anthropic, “Hooks reference — JSON output and decision control.” code.claude.com/docs/en/hooks#json-output ↩↩↩↩↩↩↩
-
Anthropic, “Hooks reference — Configuration: hook locations, matcher patterns, hook handler fields, the /hooks menu.” code.claude.com/docs/en/hooks#configuration ↩↩↩↩↩↩↩
-
Anthropic, “Hooks guide — Limitations and troubleshooting.” code.claude.com/docs/en/hooks-guide#limitations-and-troubleshooting ↩↩↩↩↩
-
Anthropic, “Hooks reference — Security considerations.” code.claude.com/docs/en/hooks#security-considerations ↩
-
Anthropic, “Claude Code settings.” code.claude.com/docs/en/settings ↩