훅으로 액션 자동화하기
훅으로 액션 자동화하기
훅(Hook)은 사용자가 정의한 셸 명령으로, Claude Code가 생명주기의 특정 시점에 자동 실행합니다. 이는 결정적 제어를 제공합니다 — LLM이 실행을 선택하도록 맡기는 대신 특정 액션이 항상 일어나도록 보장합니다. 프로젝트 규칙 준수, 반복 작업 자동화, 파일 편집 후 포맷팅, 알림 전송, 명령 검증, 컨텍스트 주입 등을 훅으로 처리할 수 있습니다. 판단이 필요한 결정은 프롬프트 기반 훅이나 에이전트 기반 훅으로 Claude 모델이 조건을 평가하게 할 수도 있습니다.
출처: 공식문서
본문
첫 훅 설정하기
~/.claude/settings.json에 hooks 블록을 추가합니다. 예: Claude가 입력을 기다릴 때 데스크톱 알림을 띄우는 Notification 훅:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
기존 hooks 키가 있으면 전체를 교체하지 말고 형제 키로 추가하세요. /hooks로 훅 브라우저를 열어 등록을 확인합니다(/hooks 메뉴는 읽기 전용 — 추가·수정·삭제는 설정 JSON을 직접 편집). 테스트는 Shift+Tab으로 수동 모드에 들어가 권한이 필요한 작업을 시킨 뒤 터미널에서 벗어나면 알림을 받습니다.
자동화할 수 있는 것
클로드가 입력 필요할 때 알림 — Notification 이벤트. 빈 matcher는 모든 알림 유형에 동작하고, 특정 유형만은 값으로 제한합니다: permission_prompt(권한 프롬프트 약 6초 대기), idle_prompt(응답 후 60초 무입력), auth_success, elicitation_*, agent_needs_input, agent_completed, quota_auto_resume_*. macOS는 osascript, Linux는 notify-send, Windows는 PowerShell MessageBox를 사용합니다.
편집 후 코드 자동 포맷 — PostToolUse 이벤트 + Edit|Write matcher. 편집된 파일 경로를 jq로 추출해 Prettier로 포맷:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }]
}
]
}
}
보호 파일 편집 차단 — PreToolUse 훅이 .env, package-lock.json, .git/ 같은 파일을 수정하려 하면 스크립트가 exit 2로 차단하고 stderr에 이유를 써서 Claude에 피드백으로 전달합니다.
통합 후 컨텍스트 재주입 — 컨텍스트 축약 후 중요 정보가 사라질 수 있으므로, SessionStart 훅 + compact matcher로 컴팩션 후마다 stdout의 평문을 Claude 컨텍스트에 추가합니다:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [{ "type": "command", "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'" }]
}
]
}
}
구성 변경 감사 — ConfigChange 이벤트(외부 프로세스·에디터가 구성 파일을 수정할 때)로 변경을 감사 로그에 기록하거나 exit 2/{"decision":"block"}으로 차단. matcher는 user_settings/project_settings/local_settings/policy_settings/skills.
디렉토리·파일 변경 시 환경 재로드 — direnv 같은 도구는 Claude의 Bash 도구가 자동 반영하지 않으므로, SessionStart + CwdChanged 훅이 direnv export bash > "$CLAUDE_ENV_FILE"을 쓰면 Claude Code가 각 Bash 명령 전 스크립트 프리앰블로 실행합니다. 특정 파일(.envrc|.env)만 보려면 FileChanged 훅을 사용하세요.
특정 권한 프롬프트 자동 승인 — PermissionRequest 훅이 stdout에 JSON 결정을 씁니다. ExitPlanMode만 자동 승인:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [{ "type": "command", "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'" }]
}
]
}
}
matcher는 최대한 좁게 유지하세요(.*나 빈 값은 모든 도구 권한 프롬프트를 자동 승인). updatedPermissions의 setMode로 특정 권한 모드(default, acceptEdits, bypassPermissions)로 전환할 수도 있습니다.
훅 작동 방식
Claude Code는 생명주기의 특정 지점에서 훅 이벤트를 실행합니다. 주요 이벤트:
| 이벤트 | 발생 시점 |
|---|---|
SessionStart |
세션 시작·재개 시 |
Setup |
--init-only 또는 -p 모드의 --init/--maintenance |
UserPromptSubmit |
프롬프트 제출 시, 처리 전 |
PreToolUse |
도구 호출 실행 전 (차단 가능) |
PermissionRequest |
도구 호출이 권한 결정 필요할 때 |
PermissionDenied |
auto mode가 도구 호출 거부 시 |
PostToolUse |
도구 호출 성공 후 |
PostToolUseFailure |
도구 호출 실패 후 |
Notification |
알림 전송 시 |
SubagentStart/SubagentStop |
서브에이전트 시작/종료 |
Stop |
Claude 응답 완료 시 |
StopFailure |
API 오류로 턴 종료 시 |
InstructionsLoaded |
CLAUDE.md·규칙 파일이 컨텍스트에 로드될 때 |
ConfigChange |
구성 파일 변경 시 |
CwdChanged |
작업 디렉토리 변경 시 |
FileChanged |
감시 중인 파일이 디스크에서 바뀔 때 |
PreCompact/PostCompact |
컨텍스트 축약 전/후 |
PreModelSwitch/PostModelSwitch |
모델 전환 전/후 |
SessionEnd |
세션 종료 시 |
대부분이 "type": "command"(셸 명령)이며, 그 외 4가지 타입: http(URL로 POST), mcp_tool(연결된 MCP 서버의 도구 호출), prompt(단일 턴 LLM 평가), agent(도구 접근 있는 다중 턴 검증, 실험적).
여러 훅 결과 결합: 같은 이벤트에 여러 훅이 매치되면 전부 완료까지 실행됩니다. 한 훅의 deny가 형제 훅을 막지 못합니다. PreToolUse 권한 결정은 제한적 순서 deny → defer → ask → allow로 적용됩니다.
입력 읽기·출력 반환: 이벤트 발생 시 JSON을 stdin으로 전달하고, 스크립트는 exit code와 stdout/stderr로 응답합니다. 공통 필드: session_id, cwd, hook_event_name, tool_name, tool_input. 예:
{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" }
}
Exit code 의미:
- Exit 0: 이의 없음.
PreToolUse는 도구 승인을 뜻하지 않음(일반 권한 흐름 적용).UserPromptSubmit/UserPromptExpansion/SessionStart/PostModelSwitch는 평문 stdout을 Claude 컨텍스트에 추가 - Exit 2: 액션 차단. stderr에 이유를 작성 (Claude 피드백/사용자 표시/무표시는 이벤트별 상이)
- 기타 exit code: stdout이 스키마 통과 JSON이면 그 JSON이 결과 결정(exit code 무시). 실패한 JSON/평문이면 비차단 오류 처리
구조화 JSON 출력 — 정밀 제어를 위해 exit 0 + stdout에 JSON:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
permissionDecision 값: allow(인터랙티브 프롬프트 건너뜀 — deny/ask 규칙·requiresUserInteraction MCP는 여전히 적용), deny(도구 호출 취소 + 이유를 Claude에), ask(일반 프롬프트 표시), defer(-p 모드에서 프로세스 exit, SDK 래퍼가 입력 수집 후 재개). UserPromptSubmit는 hookSpecificOutput.additionalContext로 컨텍스트에 텍스트 주입.
matcher로 필터링: matcher 없으면 이벤트마다 항상 발화. Edit|Write는 Edit·Write 도구만. (v2.1.191+부터 콤마도 대체자로 동작.) 이벤트별 필터 대상: PreToolUse 등은 도구 이름, SessionStart는 시작 방식(startup/resume/clear/compact/fork), Notification은 알림 유형, SubagentStart는 에이전트 유형, PreCompact는 manual/auto 등. MCP 도구는 mcp__<server>__<tool> 이름 규칙이라 matcher: "mcp__github__.*"처럼 정규식으로 매치.
if 필드로 도구 이름+인자 함께 필터: "if": "Bash(git *)"는 git 명령일 때만 훅 프로세스를 스폰합니다. tool 이벤트(PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied)에서만 동작합니다.
훅 위치(스코프): ~/.claude/settings.json(모든 프로젝트), .claude/settings.json(단일 프로젝트, 커밋 가능), .claude/settings.local.json(gitignore), 관리형 정책(조직 전역), 플러그인 hooks/hooks.json, 스킬·서브에이전트 프론트매터. disableAllHooks: true로 전체 비활성.
프롬프트 기반 훅
결정적 규칙 대신 판단이 필요할 때 type: "prompt" 훅을 씁니다. 셸 명령 대신 Claude 모델(기본 Haiku, model 필드로 변경)이 결정을 JSON으로 반환: {"ok": true}면 진행, {"ok": false}면 이벤트에 따라 reason을 Claude에 피드백하거나 턴 종료. 예 — 완료 여부를 모델이 판단:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}." }
]
}
]
}
}
에이전트 기반 훅
type: "agent" 훅은 파일을 읽고·코드를 검색하는 서브에이전트를 스폰해 조건을 검증합니다(실험적). "ok"/"reason" 형식, 기본 타임아웃 60초, 최대 50 툴-유즈 턴. ok: false 시 continueOnBlock: true처럼 처리되어 PreToolUse/PostToolUse에서 턴이 계속됩니다. 예 — 테스트 통과 검증:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS", "timeout": 120 }
]
}
]
}
}
HTTP 훅
type: "http" 훅은 셸 명령 대신 HTTP 엔드포인트로 이벤트를 POST합니다. 엔드포인트는 command 훅이 stdin으로 받는 것과 같은 JSON을 받고, 같은 JSON 형식으로 응답 본문에 결과를 반환합니다. 헤더 값은 $VAR_NAME/${VAR_NAME} 인터폴레이션을 지원하며 allowedEnvVars에 나열된 변수만 해석됩니다. HTTP 상태 코드만으로는 액션을 차단할 수 없습니다(2xx + hookSpecificOutput 필요).
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{ "type": "http", "url": "http://localhost:8080/hooks/tool-use", "headers": { "Authorization": "Bearer $MY_TOKEN" }, "allowedEnvVars": ["MY_TOKEN"] }
]
}
]
}
}
제한 사항·문제 해결
- Command 훅은 stdout·stderr·exit code로만 통신.
/명령·도구 호출 트리거 불가 - 타임아웃: command/http/mcp_tool 10분(
UserPromptSubmit·PreModelSwitch·PostModelSwitch30초,MessageDisplay10초), prompt 30초, agent 60초,SessionEnd1.5초 예산 PostToolUse는 이미 실행됐으므로 취소 불가PreToolUse는bypassPermissions/--dangerously-skip-permissions에서도 모든 권한 모드 앞에서 발화 — 사용자가 권한 모드를 바꿔도 우회 못 하는 정책 강제 가능. 반대로allow가 설정의 deny 규칙을 우회하지는 못함(훅은 제한만 강화, 완화는 불가)- 훅이 안 발화:
/hooks로 등록 확인, matcher가 대소문자 구분 정확 매치인지, 이벤트 타입 확인 - 훅 오류: 스크립트가 예상 밖 비-0 exit. 수동으로 sample JSON 파이프해 테스트.
command not found면 절대 경로·${CLAUDE_PROJECT_DIR}사용,chmod +x확인 /hooks가 비어 있음: 재시작으로 강제 리로드, JSON 유효성(후행 콤마·주석 금지), 위치 확인- Stop 훅 블록 상한: 8회 연속 블록 후 Claude Code가 훅을 무시하고 턴 종료. JSON의
stop_hook_active필드를 확인해 일찍 exit 0. 상한은CLAUDE_CODE_STOP_HOOK_BLOCK_CAP로 상향 - JSON이 효과 없음: JSON 앞의 추가 출력(빈
echo), 필드 위치 오류(permissionDecision은hookSpecificOutput안). 셸 프로파일의echo는 인터랙티브 셸에서만 실행되게 감싸세요(if [[ $- == *i* ]]) - 디버그:
Ctrl+O로 트랜스크립트 확인,claude --debug-file /tmp/claude.log로 디버그 로그, 미드 세션/debug