상위: 08_00_MOC

Claude Code — Hooks 컨텍스트 전처리와 강제 집행

Hooks의 본질

훅은 CLAUDE.md와 근본적으로 다르다. CLAUDE.md는 “부탁”이고 훅은 “코드로 실행되는 강제”다.

“Settings rules are enforced by the client regardless of what Claude decides to do. CLAUDE.md instructions shape Claude’s behavior but are not a hard enforcement layer.” — How Claude remembers your project, Anthropic

훅은 Claude의 판단을 우회해 특정 라이프사이클 이벤트에서 셸 명령을 실행한다.


라이프사이클 이벤트

이벤트 목록은 다음과 같다.

세션 단위

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

턴 단위

  • UserPromptSubmit — 프롬프트 제출 직후, Claude 처리 전
  • Stop — Claude가 응답 완료 시
  • StopFailure — API 오류로 턴 종료 시

도구 호출 단위 (에이전틱 루프)

  • PreToolUse — 도구 실행 전 (차단 가능)
  • PostToolUse — 도구 실행 성공 후
  • PostToolUseFailure — 도구 실행 실패 후
  • PostToolBatch — 병렬 도구 호출 배치 완료 후

기타

  • InstructionsLoaded — CLAUDE.md 또는 .claude/rules/*.md 파일 로드 시
  • SubagentStart — 서브에이전트 스폰 시
  • SubagentStop — 서브에이전트 완료 시
  • PermissionRequest — 권한 대화상자 표시 시
  • PermissionDenied — 도구 호출 거부 시

additionalContext — 컨텍스트 주입의 올바른 방법

훅이 Claude의 컨텍스트에 정보를 주입하려면 반드시 additionalContext JSON 필드를 써야 한다.

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "This file is generated. Edit src/schema.ts and run `bun generate` instead."
  }
}

주의할 점이 있다. 일반 stdout(exit 0)은 Claude 컨텍스트에 들어가지 않고 디버그 로그에만 남는다. “훅은 도는데 Claude가 못 읽는” 문제는 대개 여기서 비롯된다.

additionalContext가 컨텍스트에 주입되는 위치 (이벤트별):

  • SessionStart/SubagentStart: 대화 시작 전, 첫 프롬프트 앞
  • UserPromptSubmit: 제출된 프롬프트 옆
  • PreToolUse/PostToolUse/PostToolBatch: 도구 결과 옆
  • Stop/SubagentStop: 턴 끝 (대화 계속됨)

10,000자 한도: additionalContext를 포함한 출력 문자열은 10,000자가 상한이다. 초과분은 파일에 저장되고 그 경로가 Claude에 전달된다.

작성 원칙은 다음과 같다.

✅ "The deployment target is production"   — 사실 서술
✅ "This repo uses `bun test`"             — 사실 서술
❌ "You must use bun test"                 — 명령형 → 프롬프트 인젝션 방어 트리거

도구 출력 변환 — 토큰 절약의 핵심

PreToolUse: 입력 수정

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "command": "npm run lint"
    }
  }
}

PostToolUse: 출력 요약

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "updatedToolOutput": "Formatted summary of tool result"
  }
}

토큰 절약 실전 예 — 테스트 출력 필터링:

#!/bin/bash
# ~/.claude/hooks/filter-test-output.sh
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
 
# 테스트 실행 시 실패/에러만 필터링해 컨텍스트 절약
if [[ "$cmd" =~ ^(npm test|pytest|go test) ]]; then
  filtered_cmd="$cmd 2>&1 | grep -A 5 -E '(FAIL|ERROR|error:)' | head -100"
  echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered_cmd\"}}}"
else
  echo "{}"
fi

settings.json 연결:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/filter-test-output.sh"
          }
        ]
      }
    ]
  }
}

“Instead of Claude reading a 10,000-line log file to find errors, a hook can grep for ERROR and return only matching lines, reducing context from tens of thousands of tokens to hundreds.” — Manage costs effectively, Anthropic


강제 차단 — PreToolUse 패턴

#!/bin/bash
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  # 결정 없음 — 일반 권한 흐름 적용
fi

exit code 의미:

  • 0: JSON 처리, stdout은 디버그 로그에만
  • 2: JSON 무시, stderr 내용을 Claude에 오류 메시지로 전달 (도구 차단)

SessionStart 훅 — 세션 컨텍스트 주입

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Current branch: feat/auth-refactor\nUncommitted changes: src/auth.ts, src/login.tsx\nActive issue: #4211 Migrate to OAuth2",
    "sessionTitle": "auth-refactor"
  }
}

세션을 시작할 때마다 개발 컨텍스트를 자동으로 주입하므로, Claude가 현재 작업 상태를 곧바로 파악한다.

--continue--resume으로 재개하면 SessionStart 훅은 source: "resume"로 다시 실행되어 타임스탬프나 커밋 SHA 같은 동적 정보를 갱신할 수 있다. 반면 다른 이벤트의 additionalContext는 트랜스크립트에 저장됐다가 그대로 재생되므로 다시 실행되지 않는다.


InstructionsLoaded 훅 — 로드 디버깅

어떤 instruction 파일이 언제 로드됐는지 기록한다.

{
  "hooks": {
    "InstructionsLoaded": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Loaded: $CLAUDE_INSTRUCTION_FILE\" >> ~/.claude/instruction-log.txt"
          }
        ]
      }
    ]
  }
}

path-scoped 규칙을 디버깅하거나 온디맨드 로드 타이밍을 확인할 때 쓸모가 있다.


실패모드 매핑

  • 01 오염 완화: 대용량 도구 출력을 updatedToolOutput으로 요약 → 메인 컨텍스트 오염 방지
  • 04 충돌 완화: CLAUDE.md의 “절대 금지” 대신 PreToolUse 훅으로 결정론적 차단
  • 산만 완화: 로그·테스트 출력을 grep 필터로 압축 → 핵심 정보만 컨텍스트에

참고문헌

관련 노트