Ch5. Hooks 시스템
라이프사이클 이벤트(PreToolUse·PostToolUse·Stop 등)에서 셸·HTTP·prompt 훅을 결정적으로 실행해 도구 차단·포매팅·컨텍스트 주입을 자동화하는 방법
핵심 요약
- Hooks는 LLM 판단에 기대지 않고 라이프사이클 시점마다 항상 실행되는 결정적 자동화 수단입니다.
- 훅 타입은 셸
command,http(JSON POST),mcp_tool, LLM 판단형prompt·agent(실험적) 다섯 가지입니다. PreToolUse는 종료 코드 2로 도구 실행을 차단하고, 더 정밀한 제어는 종료 코드 0 + stdout JSON(permissionDecision)으로 합니다.if필드(v2.1.85)는Bash(git *)같은 퍼미션 규칙 구문으로matcher보다 정밀하게 실행 조건을 거릅니다.PreToolUse의deny는bypassPermissions에서도 도구를 막지만,allow는 deny 규칙을 우회하지 못합니다.
Hooks는 Claude Code의 라이프사이클 특정 시점에 자동으로 실행되는 사용자 정의 명령입니다. LLM의 판단에 기대지 않고 특정 동작이 항상 일어나도록 결정적(deterministic) 으로 제어하므로, 도구 실행 차단, 포매팅, 로깅, 컨텍스트 주입 같은 자동화를 짤 수 있습니다.
이벤트 종류
주요 이벤트(요약):
SessionStart,SessionEndSetup—-p모드에서--init-only/--init/--maintenance로 실행할 때 발생, CI·스크립트의 1회성 준비 작업에 활용UserPromptSubmitUserPromptExpansion— 사용자가 입력한 명령이 프롬프트로 확장될 때 발생, 확장을 차단할 수 있음PreToolUse,PostToolUse,PostToolUseFailurePostToolBatch— 병렬 도구 호출 배치가 모두 끝난 뒤, 다음 모델 호출 직전에 발생PermissionRequest— 퍼미션 다이얼로그가 표시될 때 발생 (헤드리스-p모드에서는 발생하지 않음)NotificationMessageDisplay— 어시스턴트 메시지가 화면에 표시되는 동안 발생 (표시 전용)Stop,SubagentStopSubagentStart— 서브에이전트가 생성될 때 발생StopFailure— API 에러로 턴이 종료될 때 발생. 출력과 종료 코드는 무시됨 (v2.1.78)TeammateIdle,TaskCompleted,TaskCreated(멀티 에이전트 워크플로우)PreCompact— compaction 직전에 발생. 최신 버전에서는 이 시점에서 compaction 자체를 차단할 수 있습니다. (v2.1.105)PostCompact— compaction 완료 후 발생, 로깅/알림에 활용 (v2.1.76)Elicitation— MCP 서버가 도구 호출 중 사용자에게 구조화된 입력을 요청할 때 발생 (v2.1.76)ElicitationResult— 사용자가 elicitation에 응답한 결과가 서버로 전송되기 직전에 발생 (v2.1.76)InstructionsLoaded— CLAUDE.md /.claude/rules/*.md파일이 컨텍스트에 로드될 때 발생. 세션 시작 시점과 세션 도중 지연 로드될 때 모두 발생 (v2.1.69)ConfigChange— 세션 중 설정 파일 변경 시 발생, 기업 보안 감사에 활용 (v2.1.49)WorktreeCreate/WorktreeRemove— worktree 생성/제거 시 발생하며 기본 git 동작을 대체하는 커스텀 VCS 설정에 활용 (v2.1.50)CwdChanged— 작업 디렉토리 변경 시(예:cd실행) 발생 (v2.1.83)FileChanged— 감시 대상 파일이 디스크에서 변경될 때 발생.matcher로 감시할 파일명을 지정 (v2.1.83)PermissionDenied— Auto 모드 분류기가 도구 실행을 거부했을 때 발생.{retry: true}응답으로 재시도 가능 (v2.1.88)
이벤트 전체 목록과 스키마
이벤트별 입력/출력 JSON 스키마, 발생 시점 표는 공식 Hooks 레퍼런스에서 확인할 수 있습니다.
Hook 타입
대부분의 훅은 셸 명령을 실행하는 command 타입을 쓰고, 그 외에 네 가지 타입을 더 지원합니다.
| 타입 | 동작 |
|---|---|
command | 셸 명령 실행. stdin으로 이벤트 JSON 수신, 종료 코드/stdout로 응답 |
http | URL에 이벤트 JSON을 POST하고 응답 본문으로 결정 수신 |
mcp_tool | 이미 연결된 MCP 서버의 도구를 호출 |
prompt | 단일 턴 LLM 평가 (기본 Haiku 모델로 yes/no 판단) |
agent | 도구 접근이 가능한 멀티 턴 검증 서브에이전트 (실험적 기능) |
command 타입
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "prettier --write ."
}
]
}
]
}
}HTTP hooks (v2.1.63)
셸 명령 대신 URL로 JSON POST를 보내고 JSON 응답을 받는 방식입니다. 외부 웹훅 서비스나 내부 API와 엮을 때 쓸모가 있습니다. command 훅이 stdin으로 받는 것과 똑같은 이벤트 JSON이 요청 본문으로 전달되고, 응답 본문은 command 훅과 같은 출력 포맷을 씁니다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "http",
"url": "https://hooks.example.com/format",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}HTTP 훅 응답 규칙
도구 호출을 차단하려면 적절한 hookSpecificOutput 필드를 담은 2xx 응답을 반환해야 합니다. HTTP 상태 코드만으로는 동작을 차단할 수 없습니다. 헤더 값에는 $VAR_NAME / ${VAR_NAME} 형태로 환경 변수를 보간할 수 있으나, allowedEnvVars에 명시한 변수만 치환됩니다.
prompt / agent 타입
판단(judgment)이 필요하면 결정적 규칙 대신 Claude 모델로 평가하는 prompt·agent 훅을 씁니다. 두 타입 모두 {"ok": true|false, "reason": "..."} 형태의 JSON으로 응답합니다.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "모든 작업이 완료됐는지 확인하라. 아니라면 {\"ok\": false, \"reason\": \"남은 작업\"}으로 답하라."
}
]
}
]
}
}agent 타입은 파일을 읽거나 명령을 실행해 실제 코드 상태를 확인한 뒤 결정을 내리지만, 실험적 기능이라 동작과 설정이 바뀔 수 있습니다. 프로덕션 워크플로우에는 command 훅을 권장합니다.
PreToolUse 차단/허용
PreToolUse 훅은 도구 실행을 차단할 수 있습니다.
- 종료 코드
0: 정상 진행 (단, 자동 승인은 아님 — 일반 퍼미션 흐름이 그대로 적용) - 종료 코드
2: 도구 실행 차단 (stderr가 Claude에 피드백으로 전달) - 그 외 종료 코드: 동작은 진행되며, transcript에 비차단 에러 알림과 stderr 첫 줄만 표시
exit 2는 이벤트마다 의미가 다릅니다
PreToolUse, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, PreCompact, ConfigChange 등에서는 exit 2가 동작을 차단합니다. 반면 PostToolUse, PostToolUseFailure, StopFailure, SessionStart, SessionEnd, Notification 등 이미 발생한 시점의 이벤트에서는 차단 효과가 없고 stderr만 표시됩니다.
구조화된 JSON 출력
exit 코드만으로는 차단과 침묵까지만 됩니다. 더 정밀하게 제어하려면 종료 코드 0으로 종료하고 stdout에 JSON을 출력합니다. exit 2로 종료하면 JSON은 무시되니 두 방식을 섞지 마세요.
모든 이벤트에 공통으로 적용되는 필드:
continue—false이면 Claude를 완전히 중단stopReason—continue: false일 때 표시할 메시지suppressOutput— transcript에서 훅 stdout 숨김systemMessage— 사용자에게 표시할 메시지
PreToolUse는 다음과 같이 hookSpecificOutput.permissionDecision으로 결정합니다.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "grep 대신 rg를 사용하세요"
}
}"allow"— 대화형 퍼미션 프롬프트를 건너뜀 (단 deny/ask 규칙은 여전히 적용)"deny"— 도구 호출 취소 후 이유를 Claude에 전달"ask"— 평소대로 퍼미션 프롬프트 표시"defer"— 헤드리스(-p) 모드 전용 (아래 고급 응답 참고)
PostToolUse/Stop 등은 최상위 decision: "block" 필드를, PermissionRequest는 hookSpecificOutput.decision.behavior를 사용하는 등 이벤트마다 결정 패턴이 다릅니다.
설정 위치
| 파일/위치 | 범위 | Git 공유 |
|---|---|---|
~/.claude/settings.json | 전역(모든 프로젝트) | X |
.claude/settings.json | 프로젝트 | O |
.claude/settings.local.json | 로컬 | X |
| Managed policy settings | 조직 전체 | O (관리자) |
플러그인 hooks/hooks.json | 플러그인 활성 시 | O |
| Skill/agent frontmatter | 컴포넌트 활성 시 | O |
실행 중에 설정 파일을 직접 고치면 파일 워처가 보통 변경을 자동으로 감지합니다. 몇 초가 지나도 반영되지 않으면 세션을 재시작하세요.
조건부 if 필드 (v2.1.85)
훅에 if 필드를 추가하면 퍼미션 규칙 구문으로 도구 이름과 인자를 함께 따져 실행 조건을 세밀하게 제어합니다. matcher가 그룹 단위로 도구 이름만 거르는 것보다 정밀하게 필터링하며, 조건이 맞을 때만 훅 프로세스가 생성됩니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "echo 'git 명령 감지' >> /tmp/git-audit.log"
}
]
}
]
}
}if 필드는 Bash(git *), Edit(*.ts)처럼 퍼미션 규칙과 똑같은 구문을 씁니다. npm test && git push 같은 복합 명령은 각 서브커맨드를 따져 하나라도 매칭되면 훅을 실행합니다.
if 필드 제약
if는 v2.1.85 이상이 필요하며, 이전 버전은 이를 무시하고 매칭된 모든 호출에서 훅을 실행합니다. 또한 if는 도구 이벤트(PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied)에서만 동작하며, 다른 이벤트에 추가하면 훅 자체가 실행되지 않습니다.
matcher 패턴
matcher는 이벤트별 필드를 거르는 필터입니다. 비워두거나 "*"로 두면 모든 발생에 매칭됩니다.
도구 이벤트(PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied)에서는 도구 이름을 기준으로 합니다.
| 패턴 | 매칭 대상 |
|---|---|
Write|Edit | Write 또는 Edit |
Bash | Bash 실행 |
mcp__.* | 모든 MCP 도구 |
.* | 모든 도구 |
matcher는 도구 이벤트 전용이 아닙니다
도구 이벤트 외에도 여러 이벤트가 자체 필드로 matcher를 지원합니다. 예: SessionStart(startup/resume/clear/compact), Notification(permission_prompt/idle_prompt/auth_success 등), SessionEnd(종료 사유), PreCompact·PostCompact(manual/auto), SubagentStart·SubagentStop(에이전트 타입), ConfigChange(설정 소스), FileChanged(감시 파일명). 반대로 UserPromptSubmit, Stop, CwdChanged, WorktreeCreate 등은 matcher를 지원하지 않고 항상 발생합니다. matcher는 대소문자를 구분합니다.
MCP 도구는 mcp__<server>__<tool> 형식을 따르므로 mcp__memory__.*(특정 서버의 모든 도구)나 mcp__.*__write.*(모든 서버의 write 도구) 같은 정규식으로 매칭할 수 있습니다.
훅 입력 필드
훅이 실행될 때 stdin으로 전달되는 입력 JSON에는 session_id, cwd, hook_event_name, tool_name, tool_input 등 공통 필드와 함께 다음이 포함됩니다.
agent_id/agent_type— 훅을 트리거한 서브에이전트의 ID와 타입 (v2.1.69)worktree— worktree 관련 정보 (name, path, branch, original repo) (v2.1.69)last_assistant_message—Stop/SubagentStop훅 입력에 포함되는 마지막 어시스턴트 메시지 (v2.1.47)permission_mode— 현재 퍼미션 모드 (default,plan,acceptEdits,bypassPermissions등)stop_hook_active—Stop훅이 이미 연속 차단을 트리거했는지 여부 (아래 Stop 훅 차단 상한 참고)
훅 응답으로 에이전트 제어
TeammateIdle/TaskCompleted 훅에서 {"continue": false, "stopReason": "..."} 형태의 JSON을 반환하면 팀메이트 에이전트를 중단시킬 수 있습니다. (v2.1.69)
환경 변수
Hooks 실행 시 다음 변수가 유용합니다.
CLAUDE_PROJECT_DIR— 프로젝트 루트 경로CLAUDE_PLUGIN_ROOT— 플러그인 설치 디렉토리CLAUDE_CODE_REMOTE— 웹(원격) 환경에서"true"(로컬에서는 미설정)CLAUDE_ENV_FILE— 환경 변수를 지속 저장하는 파일 경로.SessionStart,Setup,CwdChanged,FileChanged훅에서 사용 가능하며, 여기에export VAR=...를 기록하면 이후 Bash 명령 실행 전 preamble로 적용됨CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS—SessionEndhooks의 타임아웃(밀리초) 설정 (v2.1.74)CLAUDE_PLUGIN_DATA— 플러그인 영구 상태 저장 경로 (v2.1.78)CLAUDE_CODE_STOP_HOOK_BLOCK_CAP—Stop훅의 연속 차단 상한(기본 8회)을 조정
경로 플레이스홀더
${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}는 환경 변수이자 명령 문자열 내 경로 플레이스홀더로도 치환됩니다. 스크립트 경로를 안전하게 참조할 때 활용하세요.
실전 레시피
자동 Prettier 포매팅
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}훅은 stdin으로 이벤트 JSON을 받으므로 jq로 tool_input.file_path를 뽑아 편집된 파일에만 적용할 수 있습니다.
보호 파일 편집 차단 (예시)
.env, package-lock.json, .git/ 같은 민감 파일 수정을 막는 패턴입니다. 별도 스크립트가 대상 경로를 검사하고 exit 2로 차단하면, Claude는 stderr 메시지를 피드백으로 받아 접근을 조정합니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}Bash 실행 차단 (예시)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'Bash commands blocked by policy' >&2 && exit 2"
}
]
}
]
}
}compaction 후 컨텍스트 재주입
SessionStart의 compact matcher를 사용하면 compaction 직후 핵심 컨텍스트를 다시 주입할 수 있습니다. stdout에 출력한 텍스트가 Claude 컨텍스트에 추가됩니다.
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo '리마인더: npm 대신 Bun 사용. 커밋 전 bun test 실행.'"
}
]
}
]
}
}관리 설정과 훅 비활성화
disableAllHooks설정은 managed settings 계층을 존중합니다 — 비관리(unmanaged) 설정에서는 관리(managed) 훅을 비활성화할 수 없습니다. managed settings에 설정된 훅은 그 계층에서도disableAllHooks를 켜야 비활성화됩니다. (v2.1.49)pluginTrustMessage로 조직별 플러그인 신뢰 경고 메시지를 커스터마이즈할 수 있습니다. (v2.1.69)
PreToolUse 고급 응답 (v2.1.85)
PreToolUse 훅이 updatedInput을 JSON 응답으로 반환하면 도구 호출 인자를 자동으로 갈아끼웁니다. 사용자 입력을 자동화하거나 기본값을 주입하는 패턴에 쓸모가 있습니다. 여러 PreToolUse 훅이 동시에 updatedInput을 반환하면 마지막에 끝난 훅이 이깁니다(병렬 실행이라 순서는 비결정적). 같은 도구 입력을 여러 훅이 건드리지 않도록 주의하세요.
허용·차단 대신 defer 결정을 반환하면 헤드리스(-p) 세션은 도구 호출을 보존한 채 프로세스를 종료하므로, Agent SDK 래퍼가 입력을 모은 뒤 재개할 수 있습니다. 승인 판단을 사람 검토 뒤로 미루는 운영 흐름에 쓸모가 있습니다. (v2.1.89)
PreCompact 차단 (v2.1.105)
PreCompact 훅은 이제 compaction을 명시적으로 block할 수 있습니다.
- 프로세스 종료 코드
2 - 또는 JSON 응답
{"decision":"block"}
긴 작업의 중간 상태가 아직 정리되지 않았거나 먼저 메모리 파일·감사 로그를 남겨야 하는 운영 규칙이 있다면, PreCompact를 마지막 안전장치로 둘 수 있습니다.
Stop 훅 차단 상한
Stop/SubagentStop 훅으로 Claude의 종료를 막아 작업을 계속하게 할 수 있지만, 연속 8회 차단되면 Claude Code가 훅을 강제로 우회합니다. 무한 루프를 막으려는 장치이므로, 훅 입력의 stop_hook_active가 true면 이미 계속하도록 트리거된 상태라 일찍 exit 0하는 편이 안전합니다. 8회로 부족한 정당한 워크플로우라면 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP로 상한을 높이면 됩니다.
보안 참고
PreToolUse훅이"allow"를 반환해도deny퍼미션 규칙은 그대로 적용됩니다. 훅은 제약을 더 죌 수는 있어도 퍼미션 규칙이 허용하는 범위 너머로 풀어주지는 못합니다. 예전에는"allow"반환이deny규칙을 우회하는 버그가 있었으나 수정됐습니다. (v2.1.77)- 반대로
PreToolUse훅의"deny"는 퍼미션 모드 검사보다 먼저 실행되어,bypassPermissions모드나--dangerously-skip-permissions환경에서도 도구를 차단합니다. 사용자가 우회할 수 없는 정책 강제에 활용할 수 있습니다. PreToolUse/PostToolUse훅에 전달되는file_path가 절대 경로로 정규화됩니다. 예전에는 상대 경로가 넘어오는 경우가 있었습니다. (v2.1.88)- 훅 출력이 10,000자를 넘으면 디스크에 저장되고, 컨텍스트에는 미리보기와 파일 경로만 주입됩니다. 대용량 감사 로그나 검사 결과를 훅으로 다룰 때 세션이 비대해지는 것을 막아줍니다. (v2.1.89)
보안 주의
Hooks는 현재 사용자 권한으로 실행됩니다. 외부에서 가져온 훅/스크립트는 반드시 검토하세요.
디버깅과 트러블슈팅
/hooks메뉴(읽기 전용)로 이벤트별로 등록된 훅과 출처(User/Project/Local/Plugin/Session/Built-in)를 확인할 수 있습니다.- transcript 뷰(
Ctrl+O)는 발생한 훅별 한 줄 요약을 보여줍니다. 성공은 무음, 차단 에러는 stderr, 비차단 에러는<hook name> hook error알림과 stderr 첫 줄을 표시합니다. - 전체 실행 로그가 필요하면
claude --debug-file /tmp/claude.log로 시작하거나 세션 중/debug로 로깅을 켭니다. - 훅이 유효한 JSON을 출력하는데도 파싱 에러가 난다면, 셸 프로필(
~/.zshrc등)의 무조건적echo가 출력 앞에 끼어든 것일 수 있습니다. 비대화형 셸에서 출력하지 않도록if [[ $- == *i* ]]로 감싸세요.
참고 문서
- Hooks 가이드(시작하기·레시피): https://code.claude.com/docs/en/hooks-guide
- Hooks 레퍼런스(이벤트/스키마/JSON 출력): https://code.claude.com/docs/en/hooks
- 설정 파일 위치: https://code.claude.com/docs/en/settings