CoreCode블로그

가이드

Claude Code hooks 사용법: hook 설정, 이벤트 선택, 바로 쓰는 예제

게시일 · 최종 확인

Hooks는 Claude Code가 수명 주기의 특정 시점에 실행하는, 사용자가 정의한 셸 명령입니다. 실행 주체가 모델이 아니라 hook이므로 해당 동작은 항상 일어납니다. 이 글은 사용법 안내입니다. hook을 어디에 두는지, hook 항목은 어떤 모양인지, 어떤 이벤트가 중요한지, 그대로 붙여 넣을 수 있는 예제 네 개, 종료 코드가 동작을 막는 방식, hook이 아무것도 하지 않을 때 확인할 점을 다룹니다. Anthropic의 Automate actions with hooks와 Hooks reference를 따랐으며, 2026년 10월 30일에 두 페이지와 대조해 확인했습니다.

핵심 요약
  • hook은 설정 파일 안의 hooks 블록입니다. 이벤트를 고르고, matcher로 대상을 거른 다음, 실행할 핸들러를 정의합니다. 모든 프로젝트에 적용하려면 ~/.claude/settings.json에, 저장소와 공유하려면 .claude/settings.json에 둡니다.
  • Claude Code에서 /hooks를 입력하면 hook이 올바른 이벤트 아래 등록되었는지 확인할 수 있습니다.
  • 종료 코드 0은 성공입니다. 종료 코드 2는 차단할 수 있는 이벤트에서 동작을 막고, stderr가 그 이유가 됩니다. 그 밖의 종료 코드는 차단하지 않는 오류입니다.
  • command hook은 사용자의 전체 권한으로 셸 명령을 실행하므로, 추가하기 전에 모든 hook 명령을 검토하고 테스트하세요.

hook이란 무엇이고 언제 쓰는가

Hooks는 사용자가 정의한 셸 명령입니다. Claude Code가 수명 주기의 특정 시점에 이를 실행하므로, LLM이 실행하기로 선택하는 것에 기대지 않고 특정 동작이 항상 일어나게 하는 결정적 제어가 가능합니다. 프로젝트 규칙을 강제하고, 반복 작업을 자동화하고, Claude Code를 기존 도구와 연동하는 데 hook을 쓰세요.

Anthropic이 드는 예로는 편집 후 파일 포맷팅, 실행 전 명령 차단, Claude가 입력을 기다릴 때 알림 전송, 세션 시작 시 컨텍스트 주입이 있습니다. 결정적 규칙이 아니라 판단이 필요한 경우에는, Claude 모델로 조건을 평가하는 prompt 기반·agent 기반 hook을 가이드가 안내합니다. 세션을 시작할 때마다 컨텍스트를 주입하려면 가이드는 대신 CLAUDE.md를 권합니다. Claude에게 추가 지시와 실행 가능한 명령을 주려면 skills를 참고하세요.

보안: command hook은 사용자의 전체 권한으로 셸 명령을 실행합니다. 사용자 계정이 접근할 수 있는 모든 파일을 수정, 삭제하거나 읽을 수 있습니다. 설정에 추가하기 전에 모든 hook 명령을 검토하고 테스트하세요.

hook을 설정하는 위치

hook은 JSON 설정 파일에 정의합니다. 어디에 추가하느냐에 따라 적용 범위가 정해집니다.

위치범위공유 가능 여부
~/.claude/settings.json내 모든 프로젝트아니요, 내 컴퓨터에만 적용
.claude/settings.json단일 프로젝트예, 저장소에 커밋 가능
.claude/settings.local.json단일 프로젝트아니요, Claude Code가 설정을 저장할 때 gitignore 처리됨
관리형 정책 설정조직 전체예, 관리자가 제어
플러그인 hooks/hooks.json플러그인이 활성화된 동안예, 플러그인에 포함
스킬 frontmatter스킬이 호출된 뒤 세션의 나머지 동안예, 스킬 파일에 정의
서브에이전트 frontmatter해당 서브에이전트가 실행되는 동안예, 서브에이전트 파일에 정의

Claude Code에서 /hooks를 입력하면 설정된 hook을 읽기 전용으로 볼 수 있습니다. 목록에는 각 hook의 출처가 사용자 설정, 프로젝트 설정, 로컬 설정, 플러그인, 현재 세션 등으로 표시됩니다. hook을 끄려면 설정 파일에 "disableAllHooks": true를 지정합니다. Claude Code 실행 중에 설정 파일을 직접 편집하면 파일 감시기가 보통 hook 변경을 자동으로 반영합니다.

대화형 세션에서 Claude Code는 해당 폴더, 또는 신뢰가 그 폴더까지 미치는 상위 디렉터리에 대한 작업 공간 신뢰 대화상자를 수락하기 전까지, 내 ~/.claude/settings.json을 포함한 모든 설정 파일의 hook을 보류합니다.

hook 항목의 구조

설정은 3단계로 중첩됩니다. 반응할 hook 이벤트(PreToolUse, Stop 등)를 고르고, 발동 조건을 거르는 matcher 그룹("Bash 도구에서만" 등)을 추가한 다음, 일치했을 때 실행할 hook 핸들러를 하나 이상 정의합니다. 각 이벤트 이름은 하나뿐인 hooks 객체 안의 키입니다. 다음은 Anthropic 가이드의 첫 hook인 macOS 데스크톱 알림입니다.

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

설정 파일에 이미 hooks 키가 있다면 객체 전체를 바꾸지 말고, 기존 이벤트 키와 같은 층에 새 이벤트를 추가하세요.

안쪽 hooks 배열의 각 객체가 hook 핸들러입니다. 대부분의 hook은 셸 명령을 실행하는 "type": "command"를 씁니다. 그 밖에 네 가지 type이 있습니다. "type": "http", "type": "mcp_tool", "type": "prompt", "type": "agent"입니다. agent hook은 실험적이며 바뀔 수 있습니다. 공통 핸들러 필드는 다음과 같습니다.

필드필수설명
type예"command", "http", "mcp_tool", "prompt", "agent"
if아니요이 hook이 실행될 조건을 거르는 권한 규칙 문법. 예: "Bash(git *)", "Edit(*.ts)". 도구 이벤트(PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied)에서만 평가됩니다. 다른 이벤트에서는 if가 설정된 hook은 실행되지 않습니다
timeout아니요취소하기까지의 초 단위 시간. 기본값: command, http, mcp_tool은 600, prompt는 30, agent는 60
statusMessage아니요hook이 실행되는 동안 표시되는 사용자 지정 스피너 메시지

Matcher

matcher가 없으면 hook은 해당 이벤트가 일어날 때마다 발동합니다. matcher 필드는 hook이 발동하는 조건을 거르며, 평가 방식은 포함된 문자에 따라 달라집니다.

Matcher 값평가 방식예시
"*", "", 또는 생략모두 일치이벤트가 일어날 때마다 발동
문자, 숫자, _, -, 공백, ,, |만 포함정확한 문자열, 또는 |나 ,로 구분한 정확한 문자열 목록(앞뒤 공백 허용)Bash는 Bash 도구에만 일치하고, Edit|Write와 Edit, Write는 각각 두 도구 중 하나에 정확히 일치
그 밖의 문자를 하나라도 포함JavaScript 정규식, 앵커 없음^Notebook은 이름이 Notebook으로 시작하는 모든 도구에 일치하고, mcp__memory__.*는 memory 서버의 모든 도구에 일치

이벤트 종류마다 일치 대상 필드가 다릅니다. PreToolUse와 PostToolUse는 도구 이름, SessionStart는 세션이 시작된 방식(startup, resume, clear, compact, fork), Notification은 알림 유형입니다. matcher는 대소문자를 구분합니다. UserPromptSubmit, Stop 등 몇몇 이벤트는 matcher를 지원하지 않으며 항상 매번 발동합니다.

if 필드는 도구 이름으로만 그룹 수준에서 거르는 matcher보다 더 세밀합니다. 다음 설정은 모든 Bash 명령이 아니라 Claude가 git 명령을 쓸 때만 hook을 실행합니다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
          }
        ]
      }
    ]
  }
}

이 필터는 최선 노력(best-effort) 방식이므로, 허용 또는 거부를 확실히 강제하려면 hook이 아니라 권한 시스템을 쓰라고 가이드는 안내합니다.

가장 자주 쓰는 이벤트

이벤트가 발생하면 Claude Code는 일치하는 hook을 병렬로 실행합니다. 가이드에는 이보다 훨씬 많은 이벤트가 나와 있으며, 아래는 이 글의 예제가 사용하는 것과 흔히 쓰는 몇 가지입니다.

이벤트발동 시점차단 가능 여부
SessionStart세션이 시작되거나 재개될 때아니요. 종료 코드 2는 stderr를 사용자에게만 표시
UserPromptSubmit프롬프트가 제출되고 Claude가 처리하기 전예, 프롬프트를 차단
PreToolUse도구 호출이 실행되기 전예, 도구 호출을 차단
PermissionRequest도구 호출에 권한 결정이 필요할 때아니요. 종료 코드 2는 적용되지 않으며, 대신 decision 객체로 거부
PostToolUse도구 호출이 성공한 뒤아니요. stderr를 Claude에게 표시하며, 도구는 이미 실행된 상태
NotificationClaude Code가 알림을 보낼 때아니요. 종료 코드와 stderr는 무시됨
StopClaude가 응답을 마쳤을 때예, Claude가 멈추지 않고 대화를 이어가게 함
ConfigChange세션 중 설정 파일이 바뀔 때예, 변경을 차단(policy_settings 제외)
CwdChanged작업 디렉터리가 바뀔 때. 예: Claude가 cd 명령을 실행했을 때아니요. stderr를 사용자에게만 표시
PreCompact컨텍스트 압축 전예, 압축을 차단
SessionEnd세션이 종료될 때아니요. stderr를 사용자에게만 표시

붙여 넣어 쓰는 예제 네 가지

각각 Anthropic 가이드에서 가져온, 바로 쓸 수 있는 설정 블록입니다. Bash 예제는 JSON 파싱에 jq를 씁니다. macOS에서는 brew install jq, Debian과 Ubuntu에서는 apt-get install jq로 설치하거나, jq 다운로드 페이지를 참고하세요.

Claude가 입력을 기다릴 때 알림 받기

이 hook은 Claude가 입력이나 권한을 기다릴 때 Claude Code가 발생시키는 Notification 이벤트를 씁니다. macOS 버전은 위 절에 있고, 여기에는 notify-send를 쓰는 Linux 버전입니다. ~/.claude/settings.json에 추가하세요.

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
          }
        ]
      }
    ]
  }
}

빈 matcher는 모든 알림 유형에서 발동합니다. 특정 경우에만 발동하게 하려면 알림 유형 중 하나를 지정하세요. 예를 들어 permission_prompt(Claude가 도구 사용 승인을 요청할 때)나 idle_prompt(Claude가 약 60초 전에 응답을 마쳤고 그 뒤로 입력이 없을 때)가 있습니다. macOS에서 알림이 나타나지 않는다면, osascript는 알림을 내장 Script Editor 앱을 거쳐 보내는데 Script Editor에 알림 권한이 없으면 명령이 조용히 실패합니다. Linux에서 notify-send는 데스크톱 알림 데몬이 필요하며, 헤드리스 서버, SSH 세션, 대부분의 컨테이너에는 없습니다.

편집 후 코드 자동 포맷팅

이 hook은 Edit|Write matcher와 함께 PostToolUse 이벤트를 쓰므로 파일 편집 도구 다음에만 실행됩니다. 명령은 jq로 편집된 파일 경로를 추출해 Prettier에 넘깁니다. 프로젝트 루트의 .claude/settings.json에 추가하세요.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

테스트하려면 Claude에게 JavaScript 파일에 작은따옴표 문자열이 있는 줄을 추가하라고 한 뒤 파일을 열어 보세요. Prettier 기본 설정이면 hook이 이를 큰따옴표로 바꿉니다. hook이 성공해도 Claude Code는 대화에 아무것도 표시하지 않으므로, 편집된 파일이 다시 포맷되었는지 확인하세요. Bash 명령도 파일을 고쳐 쓸 수 있습니다. 어떤 방식으로 바뀌든 특정 파일을 다시 포맷하려면 FileChanged hook을 대신 쓰세요.

보호 대상 파일 편집 차단

Claude가 .env, package-lock.json, .git/ 안의 파일 같은 민감한 파일을 수정하지 못하게 합니다. Claude는 편집이 차단된 이유를 설명하는 피드백을 받으므로 접근 방식을 조정할 수 있습니다. hook은 대상 파일 경로를 보호 패턴 목록과 대조하는 스크립트를 호출하고, 종료 코드 2로 편집을 차단합니다. 스크립트를 .claude/hooks/protect-files.sh에 저장하세요.

#!/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

Claude Code가 hook 스크립트를 실행하려면 실행 권한이 있어야 합니다. macOS와 Linux에서는 다음과 같이 합니다.

chmod +x .claude/hooks/protect-files.sh

그런 다음 .claude/settings.json에 Edit 또는 Write 도구 호출 전에 스크립트를 실행하는 PreToolUse hook을 추가합니다.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

테스트하려면 Claude에게 .env 파일에 주석을 추가하라고 해 보세요. Claude Code는 편집이 실행되기 전에 차단하고, 스크립트의 Blocked: 메시지를 피드백으로 Claude에게 전달합니다.

압축 후 컨텍스트 다시 주입

Claude의 컨텍스트 창이 차면 압축이 대화를 요약해 공간을 확보하는데, 이 과정에서 중요한 세부 사항이 사라질 수 있습니다. compact matcher를 가진 SessionStart hook을 쓰면 압축할 때마다 핵심 컨텍스트를 다시 주입할 수 있습니다. 명령이 stdout에 쓴 일반 텍스트는 Claude Code가 Claude의 컨텍스트에 추가합니다. 프로젝트 루트의 .claude/settings.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를 쓰면 최근 커밋을 보여 줍니다.

종료 코드와 차단 방식

hook은 stdin, stdout, stderr, 종료 코드로 Claude Code와 통신합니다. 이벤트가 발생하면 Claude Code는 이벤트별 데이터를 JSON으로 스크립트의 stdin에 전달합니다. Bash 명령의 경우 PreToolUse hook은 session_id, cwd, hook_event_name, tool_name, tool_input 같은 필드를 받으며, tool_input.command에 셸 명령이 들어 있습니다. 다음 스크립트는 명령을 차단합니다.

#!/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가 Claude의 피드백이 됩니다
  exit 2                                               # exit 2 = 동작을 차단
fi

exit 0  # exit 0 = 결정 없음. 일반 권한 흐름이 적용됩니다

종료 코드 0으로 끝난 hook의 stderr는 Claude에게 보이지 않습니다. PostToolUse hook에서 Claude에게 경고를 전달하려면 대신 종료 코드 2를 쓰세요. 도구가 이미 실행된 뒤에도 Claude가 stderr를 볼 수 있습니다. 도구가 이미 실행되었으므로 PostToolUse hook은 동작을 되돌릴 수 없습니다.

종료 코드로는 차단하거나 침묵하는 것밖에 할 수 없습니다. 더 세밀하게 제어하려면 0으로 종료하고 stdout에 JSON 객체를 출력하세요. hook마다 한 가지 방식만 고르세요. stderr 메시지와 함께 종료 코드 2로 차단하거나, JSON과 함께 종료 코드 0으로 구조화된 제어를 하는 것입니다. 예를 들어 PreToolUse hook은 도구 호출을 거부하고 Claude에게 이유를 알릴 수 있습니다.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep for better performance"
  }
}

"deny"이면 Claude Code는 도구 호출을 취소하고 permissionDecisionReason을 Claude에게 돌려줍니다. permissionDecision: "deny"를 반환하는 hook은 bypassPermissions 모드나 --dangerously-skip-permissions에서도 도구를 차단합니다. 같은 이벤트에 여러 hook이 일치하면 Claude Code가 결과를 병합하기 전에 모든 hook의 명령이 끝까지 실행되므로, 한 hook이 deny를 반환해도 형제 hook의 실행은 멈추지 않습니다.

hook 디버깅 방법

echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?  # 종료 코드 확인
claude --debug-file /tmp/claude.log
tail -f /tmp/claude.log

문제 해결

증상확인할 점
hook이 발동하지 않음/hooks를 실행해 올바른 이벤트 아래 나타나는지 확인하세요. matcher 패턴이 도구 이름과 정확히 일치하는지 확인하세요. matcher는 대소문자를 구분합니다. 올바른 이벤트 유형을 일으키고 있는지도 확인하세요. PreToolUse는 도구 실행 전에, PostToolUse는 실행 후에 발동합니다.
/hooks에 hook이 보이지 않음몇 초가 지나도 나타나지 않으면 세션을 다시 시작해 강제로 다시 불러오세요. JSON이 올바른지 확인하세요. 후행 쉼표와 주석은 허용되지 않습니다. 파일 위치도 확인하세요. 프로젝트 hook은 .claude/settings.json, 전역 hook은 ~/.claude/settings.json입니다.
대화 기록에 "PreToolUse hook error: ..."가 표시됨스크립트가 예기치 않게 0이 아닌 코드로 종료했습니다. 샘플 JSON을 파이프로 넘겨 직접 테스트하세요. 스크립트가 아예 실행되지 않는다면 실행 권한을 주세요. chmod +x ./my-hook.sh.
"command not found"스크립트를 참조할 때 절대 경로나 ${CLAUDE_PROJECT_DIR}를 쓰세요.
"jq: command not found"jq를 설치하거나 JSON 파싱에 Python/Node.js를 쓰세요.
hook이 JSON을 출력하는데 아무 일도 일어나지 않음다른 무언가가 먼저 stdout에 쓰고 있을 수 있습니다. 보통 셸 프로필의 무조건 실행되는 echo이며, 그러면 출력이 더 이상 {로 시작하지 않습니다. 그런 echo 문은 대화형 셸에서만 실행되도록 감싸세요. 또는 필드 위치가 잘못되었을 수 있습니다. permissionDecision은 최상위가 아니라 hookSpecificOutput 안에 있어야 합니다. claude --debug로 Claude Code를 시작하고 디버그 로그에서 Hook output does not start with { 또는 Hook JSON output had unrecognized keys를 검색하세요.
Claude가 계속 작업하고 Stop hook이 너무 많이 차단했다는 경고가 나옴Claude Code는 Claude의 도구 호출 없이 Stop hook이 연속 여덟 번 차단하면 그 hook을 무시합니다. JSON 입력의 stop_hook_active 필드를 파싱해 true이면 일찍 종료하세요.

stop_hook_active 확인은 다음과 같습니다.

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # Claude가 멈추도록 허용
fi
# ... 나머지 hook 로직

FAQ

CLAUDE.md 지시 대신 hook을 써야 하는 경우는 언제인가요?

동작이 반드시 항상 일어나야 할 때 hook을 쓰세요. hook은 Claude Code가 특정 시점에 실행하는 셸 명령이므로 LLM이 실행하기로 선택하는 데 의존하지 않습니다. 세션을 시작할 때마다 컨텍스트를 주입하려면 Anthropic 가이드는 대신 CLAUDE.md를 권합니다. compact matcher를 가진 SessionStart hook은 압축 후 컨텍스트를 다시 주입하는 예입니다.

hook은 어느 설정 파일에 넣어야 하나요?

모든 프로젝트에 적용되고 내 컴퓨터에만 두는 hook은 ~/.claude/settings.json을 쓰세요. 한 프로젝트용이며 저장소에 커밋할 수 있는 hook은 .claude/settings.json을 쓰세요. 한 프로젝트용이되 공유하지 않으려면 .claude/settings.local.json을 쓰세요. hook 항목은 설정 수준 간에 서로 덮어쓰지 않고 병합됩니다.

우회(bypass) 모드에서도 hook이 무언가를 차단할 수 있나요?

네. permissionDecision: "deny"를 반환하는 PreToolUse hook은 bypassPermissions 모드나 --dangerously-skip-permissions에서도 도구를 차단합니다. PreToolUse hook은 어떤 권한 모드 검사보다 먼저 발동하기 때문입니다. "allow"를 반환하는 hook은 설정의 거부 규칙을 우회하지 않습니다.

출처