이 페이지의 목차
확인 2026-10-18근거 자료 5건갱신 2026-10-26언급 20곳
이 페이지의 목차
확인 날짜 안내

모든 설정 예와 동작 설명은 공식 훅 가이드 Claude Code 훅 가이드(원본 날짜 미상, 수집 2026-10-18) 기준이다. 설정 블록은 원본 그대로 옮겼다. 맨 아래 "레퍼런스가 더하는 레시피"는 공식 Hooks reference Claude Code 훅 레퍼런스(원본 날짜 미상, 수집 2026-10-18) 기준이다. 이벤트별 입력 스키마와 결정 필드는 훅 이벤트 레퍼런스, 공통 입출력 규칙은 훅 입출력 규칙, 정책 훅 점검은 Claude Code 차단 훅 단단하게 만들기. 개념과 다른 기능과의 경계는 훅, 안 될 때는 Claude Code 훅 문제 해결.

언제 쓰고 언제 쓰지 않는가#

  • 쓴다: 모델이 고르기를 기대하지 않고 매번 일어나야 하는 동작. 원본 표현: "certain actions always happen rather than relying on the LLM to choose to run them." 원본 예: 편집 후 포맷, 실행 전 명령 차단, 입력이 필요할 때 알림, 세션 시작 때 컨텍스트 넣기. Claude Code 훅 가이드
  • 판단이 필요하면 셸 명령 대신 type: "prompt" 훅(훅 입력 데이터만으로 판단할 수 있을 때)이나 type: "agent" 훅(코드베이스의 실제 상태를 확인해야 할 때)을 쓴다. agent 훅은 실험 기능이라 운영 워크플로에는 command 훅을 권한다. Claude Code 훅 가이드
  • 쓰지 않는다:

준비물(필요한 것)#

필요한 것내용
Claude Code와 설정 파일 하나훅은 설정 파일의 hooks 키에 넣는다. 나만 쓰면 ~/.claude/settings.json(없으면 만든다), 프로젝트에 두면 .claude/settings.json(0단계·1단계) Claude Code 훅 가이드. 설치 절차는 이 페이지의 원본이 다루지 않는다
jq (JSON 읽는 도구)이 페이지의 Bash 예는 JSON 파싱에 jq를 쓴다. macOS는 brew install jq, Debian·Ubuntu는 apt-get install jq(레시피 1) Claude Code 훅 가이드
알림 명령 (1단계)macOS는 osascript(Script Editor 알림 권한 필요), Linux는 notify-send(데스크톱 알림 데몬 필요, 헤드리스 서버·SSH·대부분의 컨테이너에서는 안 된다), Windows는 powershell.exe(1단계 아래 OS별 메모) Claude Code 훅 가이드
스크립트 파일을 쓰는 레시피라면 실행 권한macOS·Linux에서는 chmod +x .claude/hooks/protect-files.sh처럼 실행 권한을 줘야 Claude Code가 돌릴 수 있다(레시피 2) Claude Code 훅 가이드
레시피 5를 쓰려면 direnv.envrc가 있는 디렉터리마다 direnv allow를 한 번 실행해야 한다 Claude Code 훅 가이드

0단계: 어디에 둘지 고르기#

위치범위공유
~/.claude/settings.json내 모든 프로젝트아니오
.claude/settings.json프로젝트 하나예, 저장소에 커밋 가능
.claude/settings.local.json프로젝트 하나아니오
Managed policy settings조직 전체예, 관리자 통제
플러그인 hooks/hooks.json플러그인이 켜져 있을 때예
스킬 frontmatter스킬을 부른 뒤 세션 끝까지예
서브에이전트 frontmatter그 서브에이전트가 도는 동안예

근거: Claude Code 훅 가이드. 원본은 알림·설정 감사·direnv·자동 승인 예를 ~/.claude/settings.json에, 포맷·보호 파일·컴팩션 뒤 재주입 예를 프로젝트 .claude/settings.json에 둔다. 조직 단위 배포는 Claude Code managed settings 배포·점검.

1단계: 첫 훅 만들기 (데스크톱 알림)#

  1. ~/.claude/settings.json을 열어(없으면 만든다) Notification 훅을 넣는다. 아래는 macOS용 osascript 예다.

    json
    {
      "hooks": {
        "Notification": [
          {
            "matcher": "",
            "hooks": [
              {
                "type": "command",
                "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
              }
            ]
          }
        ]
      }
    }

    이미 hooks 키가 있으면 객체를 통째로 바꾸지 말고 Notification을 기존 이벤트 키의 형제로 넣는다. 이벤트 이름은 하나뿐인 hooks 객체 안의 키다:

    json
    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Edit|Write",
            "hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }]
          }
        ],
        "Notification": [
          {
            "matcher": "",
            "hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" }]
          }
        ]
      }
    }

    원하는 것을 CLI에서 설명해 Claude에게 훅을 써 달라고 할 수도 있다.

  2. Claude Code 입력창에서 /hooks를 입력해 훅 브라우저를 연다. Notification 아래에 새 훅이 보이면 된다.

  3. Esc로 돌아와 상태 표시줄이 ⏸ manual mode on이 될 때까지 Shift+Tab을 누르고, 권한이 필요한 일을 시킨 뒤 터미널에서 다른 창으로 옮긴다. 데스크톱 알림이 와야 한다.

근거: Claude Code 훅 가이드

알림이 안 올 때 (원본 OS별 메모):

  • macOS: osascript는 내장 Script Editor 앱을 거쳐 알림을 보낸다. Script Editor에 알림 권한이 없으면 명령이 조용히 실패하고 macOS가 묻지도 않는다. 터미널에서 osascript -e 'display notification "test"'를 한 번 실행해 Script Editor가 알림 설정 목록에 나오게 한 뒤, System Settings > Notifications에서 Script Editor의 Allow Notifications를 켠다.
  • Linux: 같은 자리에 "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"를 쓴다. notify-send는 데스크톱 알림 데몬이 필요해 헤드리스 서버, SSH 세션, 대부분의 컨테이너에서는 안 된다. 명령이 없으면 Debian·Ubuntu에서 libnotify-bin을 설치한다.
  • Windows(PowerShell): 원본 명령은 "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""다. 알림이 아니라 대화 상자를 띄우므로 터미널 창 뒤에 열릴 수 있다. WSL에서는 Windows interop으로 powershell.exe가 PATH에 있어야 한다.

근거: Claude Code 훅 가이드

알림 종류 고르기: 빈 matcher는 모든 알림 종류에 실행된다. 특정 경우만 받으려면 permission_prompt(도구 사용 승인 또는 샌드박스 명령의 네트워크 요청 승인이 필요하고 확인 창이 약 6초 기다렸을 때), idle_prompt(Claude가 응답을 마친 지 약 60초가 지났고 그동안 입력이 없을 때), agent_completed(agent view가 열려 있는 동안 백그라운드 세션이 끝나거나 실패했을 때) 같은 값을 쓴다. 원본 표에는 MCP elicitation, 사용 한도 뒤 자동 재개(v2.1.234 이상) 관련 값도 있다. permission_prompt 시점은 터미널과 Claude Desktop·VS Code 확장 같은 Agent SDK 호스트에서 다르다. Claude Code 훅 가이드

레시피 1: 편집한 파일 자동 포맷#

PostToolUse + Edit|Write matcher라 파일 편집 도구 뒤에만 돈다. jq로 편집한 파일 경로를 뽑아 Prettier에 넘긴다. 프로젝트 루트의 .claude/settings.json:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}
  • 시험: JavaScript 파일에 작은따옴표 문자열 한 줄을 넣어 달라고 한 뒤 파일을 연다. Prettier 기본 설정이면 큰따옴표로 바뀌어 있다.
  • 성공해도 대화에는 아무것도 보이지 않는다. 파일이 다시 포맷됐는지로 확인한다.
  • 한계: 이 matcher는 Edit·Write 도구만 본다. Bash 명령이 다시 쓴 파일까지 포맷하려면 FileChanged 훅을 쓴다.
  • 이 페이지의 Bash 예는 JSON 파싱에 jq를 쓴다. macOS는 brew install jq, Debian·Ubuntu는 apt-get install jq.

근거: Claude Code 훅 가이드

레시피 2: 보호 파일 편집 막기#

.env, package-lock.json, .git/ 안의 파일을 Claude가 고치지 못하게 한다. 막히면 Claude가 이유를 피드백으로 받아 접근을 바꿀 수 있다.

  1. .claude/hooks/protect-files.sh에 저장한다:

    bash
    #!/bin/bash
    # protect-files.sh
    
    INPUT=$(cat)
    FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
    
    # Normalize Windows backslash separators so the patterns below match
    FILE_PATH="${FILE_PATH//\\//}"
    
    PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
    
    for pattern in "${PROTECTED_PATTERNS[@]}"; do
      if [[ "$FILE_PATH" == *"$pattern"* ]]; then
        echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
        exit 2
      fi
    done
    
    exit 0
  2. macOS·Linux에서는 실행 권한을 준다. 실행할 수 없는 스크립트는 Claude Code가 돌리지 못한다: chmod +x .claude/hooks/protect-files.sh

  3. .claude/settings.json에 PreToolUse 훅으로 등록한다:

    json
    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Edit|Write",
            "hooks": [
              {
                "type": "command",
                "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
              }
            ]
          }
        ]
      }
    }
  4. 시험: .env 파일에 주석을 달아 달라고 한다. Claude Code가 편집 전에 막고 스크립트의 Blocked: 메시지를 Claude에게 피드백으로 넘긴다.

근거: Claude Code 훅 가이드

이 레시피가 막지 못하는 것 (원본이 다른 절에서 밝힌 조건):

  • matcher가 Edit|Write라 Claude가 셸 명령으로 만들거나 고친 파일은 이 훅이 보지 않는다. 모든 파일 변경을 봐야 하면 원본은 턴마다 작업 트리를 한 번 훑는 Stop 훅을 더하거나, 호출마다 보려면 Bash|PowerShell도 맞추고 스크립트에서 git status --porcelain으로 수정·미추적 파일을 나열하라고 한다. 쓴 주체와 상관없이 특정 파일이 디스크에서 바뀔 때 돌리려면 FileChanged. Claude Code 훅 가이드
  • 스크립트는 경로에 패턴 문자열이 들어 있는지(*"$pattern"*)로 판단한다. 편집자 해석: 그래서 .env가 들어간 다른 이름(예: .envrc)도 막히고, 패턴 목록에 없는 민감 파일은 막지 않는 것으로 볼 수 있다. 근거는 원본 스크립트에서 경로를 *"$pattern"*와 비교하는 if 줄이다.
  • 훅은 설정으로 꺼질 수 있다. "disableAllHooks": true는 설정 우선순위를 적용한 뒤의 값을 읽으므로 프로젝트 설정이 사용자 설정을 덮을 수 있고, managed settings의 훅은 managed settings에도 disableAllHooks가 있어야 꺼진다. Claude Code 훅 가이드
  • 훅 가이드: 설치한 mod가 tool.check를 다루면, managed settings에 있지 않은 PreToolUse 훅이 막은 호출을 승인할 수 있다(plan 모드에서는 읽기 전용 호출만). mod보다 우선하는 규칙은 권한 문서로 넘긴다. Claude Code 훅 가이드
    • (2026-10-21 기준) 권한 문서는 기준을 훅의 위치가 아니라 세션 조건으로 적는다. 기기에 managed settings가 있거나 Team·Enterprise /login이 세션 자격 증명이면 훅이 막은 호출은 훅이 어느 설정 파일에 있든 막힌 채 남는다. 둘 다 없으면(API 키·CLAUDE_CODE_OAUTH_TOKEN 세션 포함) mod가 훅이 막은 호출과 deny 규칙이 거부한 호출도 승인할 수 있다. 이 레시피는 권한 문서 기준으로 설명한다(가이드 표현과의 관계는 훅의 ‘원본끼리 달랐던 점’ 상자). 고객 기기에 이 레시피를 둘 때는 managed settings 배포 여부와 로그인 방식을 함께 확인한다. Claude Code 권한 설정
  • 원본은 if 필드 절에서, 거르기가 최선 노력이므로 확실한 허용·거부는 권한 시스템으로 하라고 한다. 편집자 해석: 보호 파일이 정말 민감하면 이 훅과 함께 permissions.deny 규칙을 두는 편이 안전하다고 볼 수 있다. 근거는 원본의 "use the permission system rather than a hook to enforce a hard allow or deny" 문장이다. → 권한 모드
  • (공식 레퍼런스, 2026-10-18 기준) protect-files.sh 경로를 잘못 쓰거나 스크립트가 죽으면 훅은 막지 않는 오류가 되고 편집은 진행된다. 실패해도 막으려면 핸들러에 "onFailure": "block"(v2.1.295 이상)을 더한다. 프롬프트에서 @로 참조한 파일은 도구 호출 없이 들어가므로 이 훅과 상관없이 읽힌다(편집이 아니라 읽기 경로). 점검 전체는 Claude Code 차단 훅 단단하게 만들기. Claude Code 훅 레퍼런스

레시피 3: 컴팩션 뒤 핵심 컨텍스트 다시 넣기#

컨텍스트 창이 차면 컴팩션이 대화를 요약하고, 원본은 이때 중요한 세부가 사라질 수 있다고 말한다. compact matcher를 단 SessionStart 훅으로 컴팩션 뒤마다 핵심 컨텍스트를 다시 넣는다. 명령이 stdout에 쓴 일반 텍스트는 Claude의 컨텍스트에 더해진다. 프로젝트 루트의 .claude/settings.json:

json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
          }
        ]
      }
    ]
  }
}
  • echo 대신 git log --oneline -5처럼 동적 출력을 내는 명령을 써도 된다.
  • 매 세션 시작 때 넣을 내용이면 CLAUDE.md를 고려한다. 환경 변수는 레퍼런스의 CLAUDE_ENV_FILE.

근거: Claude Code 훅 가이드. 컴팩션 뒤 무엇이 남고 사라지는지는 장기 작업 컨텍스트 관리.

레시피 4: 설정 변경 감사#

ConfigChange는 외부 프로세스나 편집기가 설정 파일을 고칠 때 실행된다. 준법 감사용 기록을 남기거나 허가 없는 변경을 막을 수 있다. 아래 예는 변경마다 감사 로그에 한 줄을 붙인다. ~/.claude/settings.json:

json
{
  "hooks": {
    "ConfigChange": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
          }
        ]
      }
    ]
  }
}
  • matcher는 설정 종류로 거른다: user_settings, project_settings, local_settings, policy_settings, skills.
  • 변경이 적용되지 않게 막으려면 종료 코드 2로 끝내거나 {"decision": "block"}을 돌려준다. 원본은 ConfigChange를 종료 코드 2에 메시지를 보이지 않는 이벤트의 예로 든다.
  • 시험: 세션이 도는 동안 다른 편집기로 설정 파일을 고친 뒤 ~/claude-config-audit.log를 연다. 변경마다 시각, 출처, 파일 경로가 담긴 JSON 한 줄이 붙어 있어야 한다.
  • 원본은 이 이벤트를 "외부 프로세스나 편집기"가 설정 파일을 고칠 때로 설명한다. 그 밖의 경로로 바뀐 경우도 잡는지는 원본이 말하지 않는다(전체 입력 스키마는 레퍼런스).

근거: Claude Code 훅 가이드

레시피 5: 디렉터리·파일이 바뀔 때 환경 다시 읽기#

direnv 같은 도구는 셸에서 디렉터리별 환경 변수를 자동으로 바꾸지만 Claude의 Bash 도구는 그 변화를 스스로 받지 않는다. SessionStart는 시작 디렉터리의 변수를, CwdChanged는 Claude가 디렉터리를 바꿀 때마다 변수를 다시 읽는다. 둘 다 CLAUDE_ENV_FILE에 쓰고, Claude Code는 이 파일을 Bash 명령마다 앞에 실행한다. ~/.claude/settings.json:

json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
          }
        ]
      }
    ],
    "CwdChanged": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
          }
        ]
      }
    ]
  }
}
  • .envrc가 있는 디렉터리마다 direnv allow를 한 번 실행해야 한다. devbox·nix는 direnv export bash 자리에 devbox shellenv나 devbox global shellenv를 쓴다.
  • 디렉터리 변경 대신 특정 파일에 반응하려면 FileChanged에 감시할 파일 이름을 |로 나눠 matcher로 준다. 감시 목록을 만들 때 이 값은 정규식이 아니라 문자 그대로의 파일 이름으로 쪼개진다:
json
{
  "hooks": {
    "FileChanged": [
      {
        "matcher": ".envrc|.env",
        "hooks": [
          {
            "type": "command",
            "command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
          }
        ]
      }
    ]
  }
}

근거: Claude Code 훅 가이드

레시피 6: 특정 권한 확인 자동 승인#

계획을 다 보여 준 뒤 진행해도 되는지 묻는 ExitPlanMode를 자동 승인한다. 종료 코드가 아니라 stdout에 JSON 결정을 써야 한다. PermissionRequest 훅이 "behavior": "allow"를 돌려주면 Claude Code가 대신 답한다. ~/.claude/settings.json:

json
{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "ExitPlanMode",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
          }
        ]
      }
    ]
  }
}
  • 승인되면 plan 모드를 나와 plan 모드 전의 권한 모드로 돌아간다. 트랜스크립트에는 확인 창 자리에 "Allowed by PermissionRequest hook"이 보인다. 훅 경로는 항상 현재 대화를 유지한다. 확인 창처럼 컨텍스트를 비우고 새 구현 세션을 시작하지는 못한다.
  • 특정 권한 모드로 바꾸려면 updatedPermissions 배열에 setMode 항목을 넣는다. destination: "session"은 현재 세션에만 적용한다. acceptEdits로 바꾸는 출력:
json
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow",
      "updatedPermissions": [
        { "type": "setMode", "mode": "acceptEdits", "destination": "session" }
      ]
    }
  }
}
  • bypassPermissions로 바꾸는 것은 세션을 bypass 모드가 이미 가능한 상태로 시작했을 때만 적용된다(--dangerously-skip-permissions, --permission-mode bypassPermissions, --allow-dangerously-skip-permissions, 또는 user·--settings·managed settings의 permissions.defaultMode: "bypassPermissions"). permissions.disableBypassPermissionsMode로 막혔거나 restricted mode로 시작했으면 적용되지 않는다. Claude Code는 이것을 defaultMode로 저장하지 않는다.
  • matcher는 최대한 좁게 둔다. .*로 맞추거나 matcher를 비우면 파일 쓰기와 셸 명령을 포함한 모든 도구 권한 확인을 자동 승인한다.

근거: Claude Code 훅 가이드 → Claude Code 권한 모드 고르기

레시피 7: 기록 훅과 차단 훅을 함께 걸기#

같은 이벤트에 맞는 훅은 모두 병렬로 끝까지 실행된 뒤 결과가 합쳐진다. 아래는 Bash에 PreToolUse 훅 두 개를 건다. 첫째는 모든 명령을 로그에 붙이고 종료 코드 0으로 끝나고, 둘째는 명령에 rm -rf가 있으면 종료 코드 2로 거부하는 스크립트다:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r .tool_input.command >> ~/.claude/bash.log"
          },
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
          }
        ]
      }
    ]
  }
}

Claude가 rm -rf /tmp/build를 실행하려 하면 두 훅이 병렬로 돈다. 기록 훅은 명령을 ~/.claude/bash.log에 쓰고 0으로 끝나 결정을 내지 않고, 차단 훅은 2로 끝나 거부한다. 거부가 우선하므로 명령은 막히고 Claude는 차단 훅의 stderr를 본다. 기록 훅은 이미 돌았으므로 로그 항목은 남는다. PreToolUse 권한 결정은 가장 제한적인 답이 이긴다(deny, defer, ask, allow 순). 원본 경고: 한 훅의 deny가 다른 훅의 실행을 멈추지 않으므로 그것으로 다른 훅의 부작용을 막으려 하지 않는다. 원본은 block-rm-rf.sh의 내용을 싣지 않는다. 차단 스크립트를 쓰는 법은 아래 "직접 스크립트 쓰기"의 예를 참고할 수 있다. Claude Code 훅 가이드

범위 좁히기: matcher와 if#

  • matcher가 없으면 이벤트마다 실행된다. 도구 이벤트의 matcher는 도구 이름을 거른다(Bash, Edit|Write, mcp__.*). "Edit, Write"처럼 쉼표로 나눠도 같다. 대소문자를 구분한다.
  • 원본의 다른 matcher 예: Bash 명령을 모두 기록(PostToolUse + "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"), GitHub MCP 도구 호출 기록(PreToolUse + mcp__github__.*, 도구 이름을 stderr로 써서 stdout을 JSON용으로 비워 둠), /clear 때만 정리(SessionEnd + clear, 일반 종료에는 실행 안 됨).
  • if 필드는 권한 규칙 문법으로 도구 이름과 인자를 함께 걸러, 맞을 때만 훅 프로세스를 띄운다. 예: git 명령에만 정책 검사 스크립트를 돌린다.
json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
          }
        ]
      }
    ]
  }
}
if 패턴Bash 명령훅 실행?이유
Bash(git *)git push예명령 이름이 맞음
Bash(git *)npm test && git push예하위 명령을 각각 확인, git push가 맞음
Bash(git *)echo $(git log)예$()·백틱 안 명령도 확인, git log가 맞음
Bash(git *)echo $(date)아니오맞는 하위 명령 없음
Bash(git push *)echo $(date)예명령 이름보다 더 지정한 패턴은 $()·백틱·$VAR이 있으면 일단 실행
  • Bash 입력이 어떤 명령을 실행할지 판단할 수 없으면 패턴과 상관없이 훅을 실행한다. 이 거르기는 최선 노력이므로 확실한 허용·거부는 권한 시스템으로 한다.
  • if는 도구 이벤트(PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied)에서만 동작한다. 다른 이벤트에 붙이면 훅이 실행되지 않는다. 도구 이름 여럿은 핸들러를 나눠 각각 if를 주거나 matcher의 |를 쓴다.

근거: Claude Code 훅 가이드

직접 스크립트 쓰기: 입력과 출력#

  • 이벤트가 일어나면 이벤트별 데이터가 JSON으로 stdin에 들어온다. 공통 필드는 session_id, cwd 등이고, Bash 명령에 대한 PreToolUse는 hook_event_name, tool_name, tool_input(Bash는 command 필드)을 받는다. UserPromptSubmit은 prompt 텍스트를, SessionStart는 source(startup, resume, clear, compact, fork)를 받는다. Claude Code 훅 가이드
  • 원본의 차단 스크립트 예:
bash
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q "drop table"; then
  echo "Blocked: dropping tables is not allowed" >&2  # stderr becomes Claude's feedback
  exit 2                                               # exit 2 = block the action
fi

exit 0  # exit 0 = no decision; the normal permission flow applies
종료무슨 일이 일어나나
0종료 코드로는 이의 없음. PreToolUse에서는 승인이 아니라 일반 권한 흐름이 적용된다. UserPromptSubmit·UserPromptExpansion·SessionStart·PostModelSwitch에서는 일반 텍스트 stdout이 Claude의 컨텍스트에 더해진다
2동작을 막는다. 이유는 stderr에 쓴다. 이유가 가는 곳은 이벤트마다 다르고(Claude, 사용자, 또는 보이지 않음), SessionStart 등 막을 수 없는 이벤트에서는 stderr를 사용자에게 보여 주고 실행이 계속된다
그 밖대부분의 이벤트에서 막지 않는 오류로 처리되고 동작은 진행된다. 실패 시 막도록 지정한 훅이나, 스키마 검증을 통과한 JSON을 낸 훅은 예외(JSON이 결과를 정함)
  • 더 세밀하게 통제하려면 종료 코드 0과 stdout JSON을 쓴다. 예: PreToolUse에서 거부하고 이유를 알려 준다.
json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep for better performance"
  }
}
  • UserPromptSubmit에서 컨텍스트를 넣는 예(매 프롬프트에 현재 브랜치 상태). additionalContext는 반드시 hookSpecificOutput 안에 둔다. 최상위에 두면 조용히 무시된다.
json
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Current branch: release-42. Deploy freeze until Friday."
  }
}
  • JSON은 문자열 이어 붙이기 대신 jq 같은 JSON 인코더로 만든다(따옴표·역슬래시 이스케이프). 셸 프로필이 stdout에 무언가 찍으면 JSON이 무시되니 Claude Code 훅 문제 해결을 본다.

근거: Claude Code 훅 가이드

판단이 필요한 훅: prompt·agent·HTTP#

prompt 훅 — Claude 모델에 프롬프트와 훅 입력을 보내 {"ok": true} 또는 {"ok": false, "reason": ...}를 받는다. model 필드로 다른 모델을 지정할 수 있다. 원본 예: 요청한 일이 다 끝났는지 Stop에서 확인하고, 아니면 reason을 다음 지시로 삼아 계속 일하게 한다.

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
          }
        ]
      }
    ]
  }
}

"ok": false의 효과는 이벤트마다 다르다: Stop·SubagentStop은 reason을 Claude에게 주고 계속 일하게 한다("impossible": true면 멈춤을 허용). PreToolUse는 호출을 거부하고 기본으로 턴을 끝내며 이유를 경고 줄로 보인다(continueOnBlock: true면 이유를 도구 오류로 돌려주고 계속, v2.1.210 전에는 이것이 기본). PostToolUse도 기본으로 턴을 끝낸다(continueOnBlock: true면 계속). PostToolBatch·UserPromptSubmit·UserPromptExpansion은 턴을 끝낸다. Claude Code 훅 가이드

agent 훅 (실험 기능) — 파일을 읽고 명령을 돌려야 확인할 수 있을 때 쓴다. 서브에이전트를 띄워 도구로 확인한 뒤 결정을 돌려준다. 기본 타임아웃 60초, 도구 사용 최대 50턴. impossible 필드는 없고, ok: false면 같은 이벤트의 continueOnBlock: true prompt 훅처럼 처리돼 PreToolUse·PostToolUse에서도 턴이 계속된다. 원본 예: 멈추기 전에 단위 테스트 통과 확인.

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

$ARGUMENTS는 Claude Code가 훅의 JSON 입력으로 바꿔 넣는다. 운영 워크플로에는 command 훅을 권한다. Claude Code 훅 가이드 → 서브에이전트

HTTP 훅 — 셸 명령 대신 이벤트 데이터를 HTTP 엔드포인트로 POST한다. 엔드포인트는 command 훅이 stdin으로 받는 것과 같은 JSON을 받고, 응답 본문으로 같은 형식의 JSON을 돌려준다. 원본 예: 팀 전체의 도구 사용을 기록하는 공용 감사 서비스. 로컬 기록 서비스로 모든 도구 사용을 보내는 예:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "http://localhost:8080/hooks/tool-use",
            "headers": {
              "Authorization": "Bearer $MY_TOKEN"
            },
            "allowedEnvVars": ["MY_TOKEN"]
          }
        ]
      }
    ]
  }
}
  • 2xx 응답이면 본문의 hookSpecificOutput 필드로 도구 호출을 막을 수 있다. 그 밖의 상태나 요청 실패는 막지 않는 오류로 처리되고 동작이 계속된다. 실패한 엔드포인트가 동작을 막게 하려면 훅에 onFailure: "block"을 둔다.
  • 헤더 값의 $VAR_NAME·${VAR_NAME}은 allowedEnvVars에 적은 변수만 풀리고 나머지는 비어 있다.
  • 조직 통제: managed allowedHttpHookUrls·httpHookAllowedEnvVars는 managed settings. Claude Code managed settings 배포

근거: Claude Code 훅 가이드

레퍼런스가 더하는 레시피 (2026-10-18 기준)#

아래 설정 블록과 스크립트는 공식 레퍼런스 원문 그대로다. Claude Code 훅 레퍼런스

A. if로 좁혀 위험한 rm만 막기 (exec form) — matcher로 Bash를 고르고 if로 rm * 하위 명령일 때만 스크립트를 띄운다. npm test 같은 명령에는 프로세스 자체를 띄우지 않는다. args가 있으면 셸 없이 바로 실행(exec form)돼 경로 따옴표 문제가 없다.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
            "args": []
          }
        ]
      }
    ]
  }
}
bash
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0  # no decision; normal permission flow applies
fi

chmod +x .claude/hooks/block-rm.sh로 실행 권한을 준다. Windows는 matcher를 Bash|PowerShell로 하고 if를 도구마다 따로 둔다("Bash(rm *)", "PowerShell(Remove-Item *)"). 원본은 if가 최선 노력이라 확실한 거부는 권한 규칙으로 하라고 한다.

B. 파일을 쓸 때마다 테스트를 백그라운드로 돌리고 결과만 알려 주기 (async) — Claude는 기다리지 않고 계속 일하고, 테스트가 끝나면 결과가 다음 대화 턴에 additionalContext로 들어간다.

bash
#!/bin/bash
# run-tests-async.sh

# Read hook input from stdin
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Only run tests for source files
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.js ]]; then
  exit 0
fi

# Run tests and report results to Claude via additionalContext
RESULT=$(npm test 2>&1)
EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
  MSG="Tests passed after editing $FILE_PATH"
else
  MSG="Tests failed after editing $FILE_PATH: $RESULT"
fi
jq -nc --arg msg "$MSG" '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $msg}}'
json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests-async.sh",
            "args": [],
            "async": true
          }
        ]
      }
    ]
  }
}

쓰지 않을 때: 테스트 실패로 동작을 막아야 할 때. async 훅은 decision·permissionDecision·continue가 효과가 없다. 세션이 쉬고 있으면 결과는 다음 입력까지 기다리고, -p 실행은 종료 때 도는 async 훅을 cancelled로 끝낸다.

C. 터미널 알림을 terminalSequence로 보내기 — 훅은 제어 터미널 없이 돌아 /dev/tty에 쓸 수 없다. Notification 훅이 이스케이프 시퀀스를 돌려주면 Claude Code가 대신 내보낸다. tmux·GNU screen·Windows에서도 된다. 허용 시퀀스는 OSC 0/1/2/9/99/777과 BEL이고, 대화형 세션에서 화면이 떠 있을 때만 쓰인다.

bash
#!/bin/bash
# Notification hook: ping the desktop when Claude Code needs attention.
input=$(cat)
title="Claude Code"
body=$(jq -r '.message // "Needs your attention"' <<<"$input")
seq=$(printf '\033]777;notify;%s;%s\007' "$title" "$body")
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'

원본은 이 방식이 경쟁 상태가 없다(race-free)고 말한다. 편집자 해석: 어느 터미널 앱이 OSC 777을 받는지(원본: urxvt, Ghostty, Warp)에 따라 시퀀스 번호를 고른다고 볼 수 있다(OSC 9는 iTerm2·ConEmu·Windows Terminal·WezTerm, 99는 Kitty).

D. 세션 시작 때 환경 변수 이어 주기 (CLAUDE_ENV_FILE) — SessionStart 훅에서 이 파일에 export 줄을 덧붙이면 세션의 이후 Bash 명령에 적용된다. >>로 덧붙여 다른 훅이 쓴 변수를 지우지 않는다.

bash
#!/bin/bash

if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
  echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
  echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi

exit 0

이 변수는 SessionStart·Setup·CwdChanged·FileChanged 훅만 받는다. PowerShell 명령에는 모든 줄이 단순 대입일 때만 전달되므로(v2.1.296 이상), 위 PATH 줄처럼 이스케이프 안 한 $가 있으면 PowerShell 쪽은 하나도 못 받는다.

E. 팀 스킬 저장소를 받아 같은 세션에서 쓰기 (reloadSkills) — 스킬 탐색은 보통 SessionStart 훅이 끝나기 전에 돌아서, 훅이 설치한 스킬이 첫 프롬프트 때 없을 수 있다. reloadSkills: true로 다시 스캔하게 한다. 저장소 URL은 자리표시자다.

bash
#!/bin/bash

git -C ~/.claude/skills/team-skills pull --quiet 2>/dev/null || \
  git clone --quiet https://git.example.com/your-org/team-skills.git ~/.claude/skills/team-skills

echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "reloadSkills": true}}'

→ 스킬

F. 특정 모델로 전환 막기 (PreModelSwitch, v2.1.251 이상) — matcher는 대상 모델의 정식 이름에 맞추되, 정식 이름을 모르는 커스텀 ID에는 모든 훅이 돌므로 스크립트에서 to_model을 다시 확인한다.

json
{
  "hooks": {
    "PreModelSwitch": [
      {
        "matcher": "claude-opus-4-6",
        "hooks": [
          {
            "type": "command",
            "command": "jq -e '.to_model | test(\"opus-4-6\")' > /dev/null && { echo 'Opus 4.6 is retired for this project. Use a newer model.' >&2; exit 2; }; exit 0"
          }
        ]
      }
    ]
  }
}

시험: 다른 모델 세션에서 /model claude-opus-4-6을 실행하면 모델이 그대로이고 훅이 막았다는 메시지가 보여야 한다. 모델 이름은 원본 당시 예시다. 전환 비용을 보여 주며 확인받으려면 permissionDecision: "ask"와 입력의 context_tokens를 쓴다(대화형 /model에서만 확인 창, 나머지는 거부로 처리).

G. 작업 이름 규칙 강제 (TaskCreated) — 제목이 티켓 번호로 시작하지 않으면 작업 생성을 되돌리고 이유를 Claude에게 준다.

bash
#!/bin/bash
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject')

if [[ ! "$TASK_SUBJECT" =~ ^\[TICKET-[0-9]+\] ]]; then
  echo "Task subject must start with a ticket number, e.g. '[TICKET-123] Add feature'" >&2
  exit 2
fi

exit 0

같은 방식으로 TaskCompleted에서 npm test가 실패하면 완료 표시를 막고, TeammateIdle에서 ./dist/output.js가 없으면 팀원이 쉬지 못하게 하는 예가 원본에 있다. → 훅 이벤트 레퍼런스

됐는지 확인하는 법#

  1. /hooks로 맞는 이벤트 아래에 보이는지 본다.
  2. 시험 동작을 일으키고 효과(포맷된 파일, 로그 줄, 막힌 편집)를 확인한다. 성공한 훅은 대화에 아무것도 보이지 않는다.
  3. Ctrl+O 트랜스크립트 보기에서 차단 메시지나 <hook name> hook error 알림이 있는지 본다.
  4. 자세한 실행 내역은 claude --debug-file /tmp/claude.log로 시작해 다른 터미널에서 tail -f /tmp/claude.log. 이미 시작했으면 /debug.

근거: Claude Code 훅 가이드 → Claude Code 훅 문제 해결

관련#