AI 코딩 도구 설치·레퍼런스 (3/7) — Claude Code 훅(Hooks) 완전 가이드

CLAUDE.md가 권고라면 훅은 강제다. SessionStart · PreToolUse · PostToolUse · PreCompact 실전


핵심 요약

  • 대상 독자: 훅 개념은 들어봤지만 실제로 쓴 적은 없는 Claude Code 사용자.
  • 얻을 것: 공식 /hooks 레퍼런스 기준 25+ 생명주기 이벤트의 구조, 설정 위치 4곳, 핵심 4종 이벤트를 실제 동작하는 스크립트 3개로 보이기, exit code 함정, matcher 문법, 디버깅 요령.
  • 선행 조건: Claude Code 설치 완료(설치 완전판), CLAUDE.md 기초(작성 가이드).

1. 훅은 CLAUDE.md와 무엇이 다른가

CLAUDE.md 훅(Hook)
성격 권고 (advisory) 강제 (deterministic)
주입 지점 시스템 프롬프트 뒤 user message로 Claude 동작 전후에 shell command 실행
따라질 확률 높지만 보장 없음 Exit code 2면 무조건 차단
적합한 용도 코드 스타일, 아키텍처 설명 "절대 건드리면 안 되는 경로", "매 편집 후 포맷 실행" 등 예외 없는 규칙

CLAUDE.md에 "절대 쓰기 금지"라고 적어도 Claude가 가끔 어긴다. 훅으로 걸면 어길 수 없다. 훅은 Claude가 아니라 Claude Code CLI가 직접 실행하므로 LLM의 판단을 우회한다.


2. 훅을 어디에 정의하는가

settings.json 또는 스킬/에이전트/플러그인 프론트매터에서 정의할 수 있다.

위치 범위 공유 가능
~/.claude/settings.json 모든 프로젝트 (개인)
.claude/settings.json 현재 프로젝트 ✅ (git에 commit)
.claude/settings.local.json 현재 프로젝트 (개인 전용) ❌ (.gitignore)
Managed policy settings 조직 전체 ✅ (IT 배포)
Plugin hooks/hooks.json 플러그인 활성 시
Skill/Agent frontmatter 해당 스킬/에이전트 활성 동안만

전부 비활성화는 "disableAllHooks": true. 다만 managed hooks는 사용자 설정으로 끌 수 없다 (정책 보장).


3. 생명주기 이벤트 — 전체 25+ 개

공식 레퍼런스가 정의한 훅 이벤트를 용도별로 묶으면 다음과 같다.

3.1 세션 레벨

  • SessionStart — 세션 시작/재개 시
  • SessionEnd — 세션 종료 시

3.2 턴 레벨

  • UserPromptSubmit — 사용자 프롬프트 제출 직후 (Claude 처리 전)
  • UserPromptExpansion — 슬래시 명령이 프롬프트로 전개될 때
  • Stop — Claude 응답 종료 시
  • StopFailure — API 에러로 종료 시

3.3 에이전트 루프 레벨 (가장 자주 쓰는 카테고리)

  • PreToolUse — 도구 실행 직전 (차단 가능)
  • PermissionRequest — 권한 다이얼로그 표시 시
  • PermissionDenied — auto-mode 분류기가 차단한 경우
  • PostToolUse — 도구 실행 성공 후
  • PostToolUseFailure — 도구 실행 실패 후
  • PostToolBatch — 병렬 도구 호출 전체 해소 후
  • SubagentStart / SubagentStop — 서브에이전트 시작/종료

3.4 컨텍스트 & 설정

  • InstructionsLoaded — CLAUDE.md 또는 .claude/rules/*.md 로드 시
  • ConfigChange — 설정 파일 변경 시
  • CwdChanged — 작업 디렉터리 변경 시
  • FileChanged — 감시 대상 파일이 외부에서 바뀔 때

3.5 압축 & worktree

  • PreCompact / PostCompact/compact 전후
  • WorktreeCreate / WorktreeRemove — worktree 생성/삭제

3.6 기타

  • TeammateIdle — 에이전트 팀 teammate가 idle 되기 직전
  • Notification — Claude Code가 알림 전송 시
  • TaskCreated / TaskCompleted — 태스크 생성/완료
  • Elicitation / ElicitationResult — MCP 서버가 사용자 입력 요청 시

이 글은 가장 많이 쓰는 4종에 집중한다: SessionStart, PreToolUse, PostToolUse, PreCompact.


4. 기본 구조

.claude/settings.json의 뼈대:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-bash.sh",
            "timeout": 600
          }
        ]
      }
    ]
  }
}

구성 요소: - Event 키 (PreToolUse, SessionStart 등) — 여러 개 등록 가능 - matcher — 해당 이벤트가 언제 발화할지 필터링 - hooks 배열 — 실제 실행될 핸들러 목록 - typecommand / http / mcp_tool / prompt / agent 5종

4.1 matcher 문법 (중요)

해석
"*", "", 생략 모든 이벤트 매칭 항상 발화
영문·숫자·_·|만 사용 정확 일치 또는 pipe 리스트 Bash 또는 Edit\|Write
그 외 문자 포함 JavaScript 정규식 ^Notebook 또는 mcp__memory__.*

이벤트 종류마다 매칭 기준이 다르다:

이벤트 매칭 기준
PreToolUse/PostToolUse/PermissionRequest/PermissionDenied 도구 이름 (Bash, Edit|Write, mcp__.*)
SessionStart startup / resume / clear / compact
SessionEnd clear / resume / logout / other
SubagentStart/SubagentStop 에이전트 타입 (Explore, Plan, Bash…)
PreCompact/PostCompact manual / auto
InstructionsLoaded session_start / nested_traversal / path_glob_match
UserPromptSubmit/Stop matcher 없음 (항상 발화)

4.2 type — 5가지 핸들러

타입 용도
command shell 명령 실행 (기본)
http HTTP POST 요청
mcp_tool MCP 서버의 도구 호출
prompt Claude 프롬프트로 판단 위임
agent 서브에이전트로 판단 위임 (실험)

이 글은 command 타입만 다룬다 (나머지는 공식 /hooks 레퍼런스 참고).


5. 핵심 4종 실전

5.1 SessionStart — 환경 변수 주입

세션 시작 시 환경변수를 세션 전역으로 주입할 수 있다. CLAUDE_ENV_FILE에 export 문을 쓰면 된다.

.claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/session-start.sh:

#!/bin/bash

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

RECENT_CHANGES=$(git log --oneline -5 2>/dev/null || echo "no git")
cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Recent commits:\n$RECENT_CHANGES"
  }
}
EOF

exit 0

실행 권한 부여:

chmod +x .claude/hooks/session-start.sh

additionalContext는 세션 시작 프롬프트에 추가 컨텍스트로 주입된다. 10,000자 상한.

CLAUDE_ENV_FILESessionStart, CwdChanged, FileChanged 세 이벤트에서만 사용 가능.

5.2 PreToolUse — 위험 명령 차단

공식 레퍼런스 예시를 그대로 쓴다. rm -rf 패턴이 들어간 Bash 명령을 차단한다.

.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/block-rm.sh:

#!/bin/bash

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "rm -rf는 훅에 의해 차단됩니다. 개별 파일 삭제를 사용하세요."
    }
  }'
  exit 0
fi

exit 0

동작: 1. 훅이 stdin으로 JSON을 받는다 (tool_input.command 포함) 2. rm -rf 패턴이면 permissionDecision: "deny" + 사유를 stdout에 출력 3. Claude Code가 도구 호출을 차단

if 필드로 pre-filter: {"if": "Bash(rm *)"}를 넣으면 Bash 도구 중에서도 rm * 패턴만 훅을 호출한다. 불필요한 훅 실행 줄이기에 유용.

5.3 PostToolUse — 자동 포맷

편집 후 자동으로 포맷터 실행. Write 또는 Edit 도구가 성공했을 때.

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

.claude/hooks/auto-format.sh:

#!/bin/bash

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

case "$FILE" in
  *.ts|*.tsx|*.js|*.jsx)
    prettier --write "$FILE" 2>/dev/null || true
    ;;
  *.py)
    ruff format "$FILE" 2>/dev/null || true
    ;;
  *.rs)
    rustfmt "$FILE" 2>/dev/null || true
    ;;
esac

exit 0

포맷 실패해도(exit 0) 에이전트 루프는 계속된다. 만약 lint를 Claude에게 강제로 알려주고 싶으면 stdout에 JSON으로 decision: "block" + reason을 출력한다:

if ! prettier --check "$FILE" > /tmp/lint.out 2>&1; then
  jq -n --arg reason "$(cat /tmp/lint.out)" '{
    decision: "block",
    reason: $reason,
    hookSpecificOutput: {
      hookEventName: "PostToolUse",
      additionalContext: "Lint 실패. 수정 후 재시도 필요."
    }
  }'
  exit 0
fi

5.4 PreCompact — 핸드오프 문서 자동 생성

/compact 직전에 handoff 문서를 남겨두면 세션 사이 연속성을 확보할 수 있다.

.claude/settings.json:

{
  "hooks": {
    "PreCompact": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-compact.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/pre-compact.sh:

#!/bin/bash

TIMESTAMP=$(date +%Y%m%d-%H%M%S)
HANDOFF_DIR="$CLAUDE_PROJECT_DIR/tasks/handoffs"
mkdir -p "$HANDOFF_DIR"

cat > "$HANDOFF_DIR/handoff-$TIMESTAMP.md" <<EOF

## Progress
- (compact 직전 진행 상황을 이어서 작성)

## Decisions
- (중요한 결정을 명시)

## Next Steps
- (다음 세션에서 이어갈 것)

## Blockers
- (막혀있는 이슈)
EOF

cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "PreCompact",
    "additionalContext": "Compact 전 $HANDOFF_DIR/handoff-$TIMESTAMP.md를 채워주세요."
  }
}
EOF

exit 0

Tip: 메모리 시스템을 구축한 프로젝트라면 세션별 스냅샷(tasks/sessions/)을 여기서 작성하면 /compact 후에도 세션 복원 가능.


6. 입력 JSON — 훅이 받는 것

모든 훅은 stdin으로 JSON을 받는다. 공통 필드:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/dir",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "agent_id": null,
  "agent_type": null
}

이벤트별 추가 필드: - PreToolUse / PostToolUse: tool_name, tool_input (전체 인자) - SessionStart: source (startup/resume/clear/compact), model - UserPromptSubmit: prompt (사용자가 입력한 문자열) - PreCompact: trigger (manual/auto)

각 도구의 tool_input 스키마는 공식 /hooks 페이지 중반에 자세히 있다 (Bash/Write/Edit/Read/Glob/Grep/WebFetch/WebSearch 등).


7. 출력 JSON — 훅이 돌려주는 것

stdout에 JSON을 쓰면 Claude Code가 해석한다. 훅이 아무 출력도 하지 않으면 그냥 통과.

7.1 범용 필드

{
  "continue": true,
  "stopReason": "메시지",
  "suppressOutput": false,
  "systemMessage": "사용자에게 보여줄 경고"
}
  • continue: falseClaude 자체를 정지.
  • suppressOutput: true면 stdout이 디버그 로그에 기록되지 않음.

7.2 PreToolUse 전용 — 더 풍부한 제어

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "사용자에게 보여줄 사유",
    "updatedInput": {
      "command": "npm run lint"
    },
    "additionalContext": "Claude에게 추가로 줄 컨텍스트"
  }
}

permissionDecision 4가지: - allow — 권한 프롬프트 건너뛰고 허용 - deny — 도구 호출 차단 - ask — 사용자에게 확인 요청 - defer — 현재 턴 종료 후 나중에 이어서 (SDK 용)

우선순위: deny > defer > ask > allow.

updatedInput으로 명령을 수정해서 실행시킬 수도 있다. 예: git commit을 가로채서 git commit -s로 바꾸기.


8. Exit Code 함정 (가장 자주 실수하는 곳)

Exit code 동작
0 stdout의 JSON을 파싱 (정상 경로)
2 차단(block) — stderr를 에러 메시지로
기타 비차단 에러 — stderr를 transcript에 표시하고 계속

Unix 관례와 반대exit 1은 차단하지 않는다. 정책을 강제하려면 반드시 exit 2 또는 exit 0 + JSON decision: "block" 사용.

이벤트마다 exit 2의 의미가 다르다:

이벤트 exit 2 동작
PreToolUse 도구 호출 차단
UserPromptSubmit 프롬프트 차단 + 지움
Stop/SubagentStop 정지 막고 계속
PreCompact 압축 차단
PostToolUse 차단 불가 (Claude에게 메시지 전달)
PermissionDenied/StopFailure 무시
SessionStart/Notification 사용자에게만 표시

차단이 안 되는 이벤트에서 exit 2를 써도 아무 효과가 없다. 공식 표로 반드시 확인.


9. 실전 — 훅 3개 세트업

지금까지 본 3개를 합친 최소 실용 세트. .claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh"}
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-format.sh", "timeout": 30}
        ]
      }
    ],
    "PreCompact": [
      {
        "hooks": [
          {"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-compact.sh"}
        ]
      }
    ]
  }
}

디렉터리 구조:

.claude/
├── settings.json
└── hooks/
    ├── session-start.sh
    ├── block-rm.sh
    ├── auto-format.sh
    └── pre-compact.sh

모든 스크립트에 chmod +x 부여.


10. 디버깅 — 훅이 안 뜰 때

10.1 /hooks 명령으로 확인

세션에서 /hooks를 실행하면 현재 로드된 훅 전체 목록이 뜬다. 내가 등록한 훅이 안 보이면 설정 파일 경로가 틀렸거나 JSON 파싱 에러.

10.2 --debug hooks 플래그

claude --debug "hooks"

훅 실행 로그가 실시간으로 찍힌다. 필터 예: --debug "api,hooks".

10.3 Claude에게 훅 짜달라고 요청

훅 스크립트는 Claude가 잘 쓰는 영역이다. 예:

Write a hook that runs eslint after every file edit.
Write a hook that blocks writes to the migrations/ folder.

그 뒤 .claude/settings.json을 직접 열어 /hooks로 검증.

10.4 deduplication

같은 command 문자열 / HTTP URL은 이벤트당 한 번만 실행된다. 같은 훅을 두 번 걸었는데 한 번만 실행되면 중복 제거 때문.


11. 보안 · 주의점

  1. Exit 1은 차단 안 됨 — 반드시 exit 2 또는 JSON decision: "block" 사용.
  2. stdin JSON 파싱 실패는 훅의 exit code와 무관하게 Claude Code가 무시할 수 있음. jq -r 결과 항상 체크.
  3. Timeout 기본값 — command 600s, prompt 30s, agent 60s. 긴 작업은 async: true로 백그라운드.
  4. 관리자 설정의 allowManagedHooksOnly — managed settings로 사용자 훅 전체 무효화 가능.
  5. HTTP 훅의 allowedEnvVars — 환경 변수 주입은 명시적으로 허용된 것만 interpolate.
  6. MCP 서버 미연결 시 — MCP 도구 훅은 OAuth 흐름을 트리거하지 않고 그냥 실패.

12. 반대 시나리오 — 훅을 쓰지 말아야 할 때

  • 판단이 필요한 일prompt 또는 agent 타입 훅을 쓰거나, 그냥 CLAUDE.md 규칙으로 두기. 결정이 규칙으로 정형화되지 않는다면 LLM이 판단하는 게 낫다.
  • 잦은 변경이 예상되는 규칙settings.json을 자주 고치게 되고 팀 git 충돌 빈번. 이럴 땐 CLAUDE.md 쪽이 유연.
  • 플러그인·스킬로 충분한 경우 — 세션 시작 컨텍스트 주입은 skill로도 가능. 훅은 "exception 없이 반드시" 필요한 경우에만.
  • 초보자 혼자 쓰는 개인 프로젝트 → 훅이 잘못 작동하면 모든 작업이 막힌다. 위험 차단 같은 소수 훅만 시작해서 점진 확장.

13. 다음 단계

훅이 익숙해졌다면:

  1. CLAUDE.md 작성 가이드 — 권고 레이어와의 분업.
  2. 슬래시 커맨드 총정리/hooks 명령 외 다른 디버깅 명령.
  3. Claude Code 토큰·캐시 비용 완전 해부 — 훅으로 로깅해야 보이는 비용 구조 (예정).

참고


이 글은 "AI 코딩 CLI 진입 가이드" 시리즈의 4/15 편입니다. last verified: 2026-04-25 (Claude Code 공식 Hooks 레퍼런스 기준).

댓글