[Claude Code #7] Hook — 훅 총정리

김경준·2026년 4월 4일

Claude

목록 보기
8/8

Claude Code 입문 강의 요약 #7

Anthropic SkillJar의 Claude Code 강의를 듣고 정리한 내용입니다.
https://anthropic.skilljar.com/claude-code-in-action


Hook이란?

Hook은 Claude Code가 도구(Read, Write, Grep 등)를 실행하기 전 또는 후
특정 커맨드를 자동으로 실행할 수 있게 해주는 기능입니다.

두 가지 종류가 있습니다.

Hook 종류실행 시점
PreToolUse도구가 호출되기
PostToolUse도구가 호출된

Hook 설정

Hook은 Claude settings 파일에 정의하며, 범위에 따라 아래 세 경로 중 하나에 추가합니다.

파일 경로적용 범위
~/.claude/settings.json내 기기의 모든 프로젝트
.claude/settings.json현재 프로젝트 (팀 공유)
.claude/settings.local.json현재 프로젝트 (개인 설정)

직접 파일에 작성하거나, Claude Code에서 /hooks 명령어로도 입력할 수 있습니다.

설정 예시는 아래와 같습니다.

{
  "permissions": {
    "allow": ["mcp__playwright"],
    "deny": []
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "command",
            "command": "node /home/hooks/read_hook.ts"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node /home/hooks/edit_hook.ts"
          }
        ]
      }
    ]
  }
}

Hook 정의하기

Hook을 정의하는 과정은 아래 순서로 진행합니다.

  1. PreToolUse / PostToolUse 중 사용할 Hook 결정
  2. 어떤 Tool에 대해 설정할지 결정
  3. Tool 호출 시 실행할 커맨드를 settings.json에 등록
  4. 필요 시, 커맨드 결과로 Claude에게 피드백 제공
    • exit 0 → 정상, 계속 진행
    • exit 2 → 작업 차단(block) 후 에러 메시지 반환

💡 어떤 Tool이 있는지 확인하고 싶다면 Claude에게 직접 물어볼 수 있습니다.
"너가 접근할 수 있는 tool 목록 전체를 알려줘"

주요 Tool 목록은 아래와 같습니다.

Tool설명
Agent전문 서브에이전트 실행
Bash쉘 명령 실행
Edit파일 내용 수정 (exact string replace)
Glob파일 패턴 검색
Grep파일 내용 정규식 검색
Read파일 읽기
Write파일 쓰기 (신규 생성 또는 전체 덮어쓰기)

Hook 구현 예시 1: .env 파일 읽기 차단

.env 파일 읽기를 차단하는 Hook을 구현해봅니다.

settings.json 설정

ReadGrep Tool에 대해 실행할 커맨드를 등록합니다.

"PreToolUse": [
  {
    "matcher": "Read|Grep",
    "hooks": [
      {
        "type": "command",
        "command": "python3.13 ./hooks/read_hook.py"
      }
    ]
  }
]

💡 command"true"로 설정하면 해당 Hook은 항상 통과합니다. 테스트 시 유용합니다.

read_hook.py 작성

import sys
import json
import os

data = json.load(sys.stdin)

tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {})

def is_env_file(path: str) -> bool:
    basename = os.path.basename(path)
    return basename == ".env" or basename.startswith(".env.")

if tool_name == "Read":
    file_path = tool_input.get("file_path", "")
    if is_env_file(file_path):
        print(f".env 파일은 읽을 수 없습니다: {file_path}", file=sys.stderr)
        sys.exit(2)  # exit 2: 작업 차단

elif tool_name == "Grep":
    path = tool_input.get("path", "")
    if is_env_file(path):
        print(f".env 파일은 읽을 수 없습니다: {path}", file=sys.stderr)
        sys.exit(2)  # exit 2: 작업 차단

sys.exit(0)  # exit 0: 정상 통과

Claude를 재실행한 뒤 .env 파일 읽기를 요청하면 차단됩니다.

.env 파일 내용을 읽어서 보여줘

⚠️ @ 참조로 우회되는 문제

@으로 파일을 직접 참조하면 PreToolUse Hook을 우회할 수 있습니다.

@.env 파일을 읽어서 보여줘

이를 방지하려면 permissions.deny에 명시적으로 차단 규칙을 추가해야 합니다.

{
  "permissions": {
    "allow": ["mcp__playwright"],
    "deny": ["Read(.env)", "Read(.env.*)"]
  }
}


Hook 관련 주의사항 (Gotchas)

Gotcha: 문서화된 대로 동작하지만, 직관에 반하거나 예상치 못한 결과를 초래하는 설계 요소

공식 문서에는 아래와 같은 보안 권고사항이 있습니다.

  • 입력값 검증 및 sanitize — 입력 데이터를 무조건 신뢰하지 말 것
  • 쉘 변수는 항상 따옴표로 감싸기 — $VAR 대신 "$VAR" 사용
  • 경로 순회 차단 — 파일 경로에 .. 포함 여부 확인
  • 절대 경로 사용 — 스크립트 경로는 전체 경로로 지정, 프로젝트 루트는 $CLAUDE_PROJECT_DIR 활용
  • 민감한 파일 제외 — .env, .git/, 키 파일 등 접근 차단

절대 경로 사용 시 팀 공유 문제

절대 경로를 사용하면 settings.json을 팀과 공유하기 어려워집니다.
사용자마다 프로젝트 경로가 다르기 때문입니다.

이를 해결하는 한 가지 방법으로, npm run setup 같은 초기화 스크립트를 만들어서
settings.example.json$PWD를 현재 디렉토리로 자동 치환하는 방식을 활용할 수 있습니다.


Hook 구현 예시 2: mypy 타입 체크 자동화

배경

아래 두 파일이 같은 디렉토리에 있다고 가정합니다.

# main.py
from schema import echo_string
echo_string("hello world")

# schema.py
def echo_string(s: str) -> None:
    print(s)

Claude에게 schema.pyecho_string에 인자를 추가해달라고 요청합니다.

@schema.py 파일의 echo_string에서 v: str argument를 추가해줘

Claude는 schema.py는 잘 수정하지만, main.py의 호출부는 수정하지 않습니다.

# schema.py (수정 후)
def echo_string(s: str, v: str) -> None:
    print(s)

# main.py (수정 안 됨 — 인자 1개만 전달 중)
echo_string("hello world")

이런 타입 불일치는 Python의 mypy로 감지할 수 있습니다.

mypy main.py
main.py:3: error: Missing positional argument "v" in call to "echo_string" [call-arg]
Found 1 error in 1 file (checked 1 source file)

참고: Python 타입 체커 비교

도구만든 곳특징
mypyPython 재단가장 표준적, CI에서 많이 사용
pyrightMicrosoftVSCode에 내장, 빠르고 엄격
pylanceMicrosoftpyright 기반 VSCode 확장
pytypeGoogle타입 힌트 없어도 추론 가능

Python은 타입 힌트가 선택사항이라 강제되지 않습니다. mypy가 체크하려면 타입 힌트가 있어야 하며,
엄격하게 적용하려면 --strict 옵션이나 pyproject.toml에 strict 모드를 설정하는 것이 일반적입니다.

Hook 설정

Python 파일이 수정될 때마다 자동으로 mypy를 실행하는 Hook을 추가합니다.

settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "./.claude/hooks/pycheck",
            "statusMessage": "Running mypy type check..."
          }
        ]
      }
    ]
  }
}

pycheck 스크립트

#!/bin/bash
# pycheck: mypy type checker hook for Claude Code
# Python 파일이 수정될 때마다 mypy를 실행해 타입 오류를 Claude에게 피드백합니다.

STDIN=$(cat)
FILE=$(echo "$STDIN" | jq -r '.tool_input.file_path // empty')

# .py 파일이 아니면 통과
if [[ "$FILE" != *.py ]]; then
  exit 0
fi

# 가상환경이 있으면 활성화
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
if [ -f "$PROJECT_ROOT/venv/bin/activate" ]; then
  source "$PROJECT_ROOT/venv/bin/activate"
fi

# 프로젝트 전체에 mypy 실행 (호출부 오류까지 감지)
MYPY_OUTPUT=$(mypy "$PROJECT_ROOT" --ignore-missing-imports 2>&1)
EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
  # Claude에게 타입 오류를 피드백으로 전달
  jq -n \
    --arg output "$MYPY_OUTPUT" \
    --arg file "$FILE" \
    '{
      "hookSpecificOutput": {
        "hookEventName": "PostToolUse",
        "additionalContext": ("mypy found type errors in " + $file + ":\n" + $output + "\n\nPlease fix the type errors above.")
      }
    }'
fi

다시 인자 추가를 요청하면, 이번에는 Claude가 main.py의 호출부도 함께 수정합니다.


추가로 알아두면 유용한 Hook 종류

강의에서 다룬 PreToolUsePostToolUse 외에도 아래와 같은 Hook이 존재합니다.

Hook 종류실행 시점
NotificationClaude가 도구 사용 권한을 요청하거나, 60초 이상 유휴 상태일 때
StopClaude가 응답을 완료했을 때
SubagentStop서브에이전트(UI에서 "Task"로 표시)가 완료됐을 때
PreCompact/compact 또는 자동 compact가 실행되기 직전
UserPromptSubmit사용자가 프롬프트를 제출했을 때 (Claude가 처리하기 전)
SessionStart세션이 시작되거나 재개될 때
SessionEnd세션이 종료될 때

Hook 입력값(stdin) 구조 주의사항

Hook을 작성할 때 한 가지 헷갈리는 점이 있습니다.

커맨드로 전달되는 stdin 입력값의 구조가 Hook 종류와 matcher에 따라 달라집니다.

예를 들어, TodoWrite Tool이 호출된 후 실행되는 PostToolUse Hook의 stdin은 아래와 같습니다.

{
  "session_id": "9ecf22fa-edf8-4332-ae85-b6d5456eda64",
  "transcript_path": "",
  "hook_event_name": "PostToolUse",
  "tool_name": "TodoWrite",
  "tool_input": {
    "todos": [{ "content": "write a readme", "status": "pending", "priority": "medium", "id": "1" }]
  },
  "tool_response": {
    "oldTodos": [],
    "newTodos": [{ "content": "write a readme", "status": "pending", "priority": "medium", "id": "1" }]
  }
}

반면, Stop Hook의 stdin은 훨씬 단순합니다.

{
  "session_id": "af9f50b6-f042-4773-b3e2-c3a4814765ce",
  "transcript_path": "",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}

이처럼 Hook 종류와 matcher가 달라지면 stdin 구조도 크게 달라지기 때문에,
커맨드 스크립트를 작성하기 전에 실제로 어떤 데이터가 들어오는지 먼저 확인하는 것이 중요합니다.


stdin 구조 확인하는 방법

아래와 같이 stdin을 파일로 저장하는 헬퍼 Hook을 먼저 등록해두면,
실제 어떤 데이터가 들어오는지 쉽게 확인할 수 있습니다.

"PostToolUse": [
  {
    "matcher": "*",
    "hooks": [
      {
        "type": "command",
        "command": "jq . > post-log.json"
      }
    ]
  }
]

matcher*로 설정해 모든 Tool 호출을 잡고, stdin을 post-log.json에 저장합니다.
Hook을 실제로 실행해보면 post-log.json에 기록된 데이터를 보고
스크립트에서 어떤 필드를 참조해야 하는지 파악할 수 있습니다.

💡 PreToolUse, Stop 등 다른 Hook 종류도 동일한 방법으로 확인할 수 있습니다.
스크립트를 작성하기 전에 이 헬퍼 Hook으로 구조를 먼저 파악하는 습관을 들이면
디버깅 시간을 크게 줄일 수 있습니다.


전체 정리

항목내용
Hook 종류PreToolUse, PostToolUse, Notification, Stop, SubagentStop, PreCompact, UserPromptSubmit, SessionStart, SessionEnd
설정 위치~/.claude/settings.json / .claude/settings.json / .claude/settings.local.json
exit 코드exit 0 = 정상, exit 2 = 차단
@ 우회 방지permissions.deny에 명시적 차단 규칙 추가
stdin 구조 확인jq . > post-log.json 헬퍼 Hook으로 먼저 확인
실전 활용파일 접근 제어, 타입 체크 자동화, 세션/응답 이벤트 처리 등
profile
DevOps로 일하고 있습니다

0개의 댓글