훅으로 액션 자동화하기

훅으로 액션 자동화하기

훅(Hook)은 사용자가 정의한 셸 명령으로, Claude Code가 생명주기의 특정 시점에 자동 실행합니다. 이는 결정적 제어를 제공합니다 — LLM이 실행을 선택하도록 맡기는 대신 특정 액션이 항상 일어나도록 보장합니다. 프로젝트 규칙 준수, 반복 작업 자동화, 파일 편집 후 포맷팅, 알림 전송, 명령 검증, 컨텍스트 주입 등을 훅으로 처리할 수 있습니다. 판단이 필요한 결정은 프롬프트 기반 훅이나 에이전트 기반 훅으로 Claude 모델이 조건을 평가하게 할 수도 있습니다.

출처: 공식문서

본문

첫 훅 설정하기

~/.claude/settings.jsonhooks 블록을 추가합니다. 예: 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는 최대한 좁게 유지하세요(.*나 빈 값은 모든 도구 권한 프롬프트를 자동 승인). updatedPermissionssetMode로 특정 권한 모드(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 권한 결정은 제한적 순서 denydeferaskallow로 적용됩니다.

입력 읽기·출력 반환: 이벤트 발생 시 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 래퍼가 입력 수집 후 재개). UserPromptSubmithookSpecificOutput.additionalContext로 컨텍스트에 텍스트 주입.

matcher로 필터링: matcher 없으면 이벤트마다 항상 발화. Edit|Write는 Edit·Write 도구만. (v2.1.191+부터 콤마도 대체자로 동작.) 이벤트별 필터 대상: PreToolUse 등은 도구 이름, SessionStart는 시작 방식(startup/resume/clear/compact/fork), Notification은 알림 유형, SubagentStart는 에이전트 유형, PreCompactmanual/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: falsecontinueOnBlock: 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·PostModelSwitch 30초, MessageDisplay 10초), prompt 30초, agent 60초, SessionEnd 1.5초 예산
  • PostToolUse는 이미 실행됐으므로 취소 불가
  • PreToolUsebypassPermissions/--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), 필드 위치 오류(permissionDecisionhookSpecificOutput 안). 셸 프로파일의 echo는 인터랙티브 셸에서만 실행되게 감싸세요(if [[ $- == *i* ]])
  • 디버그: Ctrl+O로 트랜스크립트 확인, claude --debug-file /tmp/claude.log로 디버그 로그, 미드 세션 /debug

더 알아보기