ai-rules 09-hooks-guide · Hooks 활용 가이드

09-hooks-guide Hooks 활용 가이드

Advisory vs. Deterministic

Critical Rule
이 규칙에는 절대 금지 항목이 포함되어 있습니다. AI 에이전트가 위반할 경우 즉시 중단합니다.

Advisory vs. Deterministic

유형 구현 방식 보장 수준
Advisory CLAUDE.md 텍스트 AI가 따르려 노력함 (컨텍스트 압박 시 무시 가능)
Deterministic Hooks (.claude/settings.json) 항상 실행됨 (AI 의지와 무관)
Deterministic Git hooks (husky) commitlint, lint-staged 등 커밋 시 자동 실행

"Unlike CLAUDE.md instructions which are advisory, hooks are deterministic and guarantee the action happens." — Anthropic 공식 문서

Hook 배포 채널

ai-rules sync는 두 채널에서 hook을 배포한다:

채널 배포 대상 예시
governance guard-branch.sh, guard-reversibility.sh, guard-freeze.sh, confirm-capture.sh, detect-mid-question.sh 보호 브랜치, R2 종합 차단, 디렉토리 잠금, 행동 패턴 감지
tooling guard-secrets.sh, guard-push-force.sh, guard-destructive-db.sh, .husky/* 시크릿 탐지, push --force 차단, DB 파괴 차단, commitlint, lint-staged

두 채널의 hook은 .claude/settings.jsonmergeJson으로 병합되어 공존한다.


Hook 후보 우선순위 표

MUST-HOOK (반드시 hook으로 이중화)

Advisory로만 있으면 컨텍스트 압박 시 우회될 수 있는 핵심 가드레일:

규칙 Hook 파일 배포 채널 감지 조건 동작
보호 브랜치 커밋 금지 guard-branch.sh governance git commit + 브랜치가 main/master/develop 차단 + 경고
git push --force 금지 guard-push-force.sh tooling push --force 또는 push -f 패턴 차단
migrate reset / DB DROP 차단 guard-destructive-db.sh tooling migrate reset, db push --force-reset, DROP DATABASE/TABLE, TRUNCATE 차단 + 수동 절차 안내
R2 가역성 종합 차단 guard-reversibility.sh governance 위 패턴 + 추가 R2 명령 종합 차단 차단 (safety-manifest 기반)

배포 채널 설계 의도: guard-push-force.shguard-destructive-db.shtooling 채널로 배포하여 governance.enabled: false인 프로젝트에도 적용한다. guard-branch.shguard-reversibility.sh는 safety-manifest.yaml 의존이 있어 governance 채널에 유지한다.

MUST-HOOK — 행동 패턴 감지 (Stop hook)

에이전트의 응답 패턴을 감지하여 규칙 위반을 교정하는 hook:

규칙 Hook 파일 Hook 유형 감지 조건 동작
불필요한 질문/대기 패턴 detect-mid-question.sh + detect-mid-question-reminder.sh Stop + UserPromptSubmit "계속할까요?", "준비 완료" 등 04-lifecycle 위반 state 기록 → 다음 턴 리마인더 주입
자가검증 없는 완료 선언 detect-premature-completion.sh + detect-premature-completion-reminder.sh Stop + UserPromptSubmit "완료했습니다" 등 05-responses 검증 마커 없이 종료 state 기록 → 다음 턴 리마인더 주입

detect-mid-question 동작 흐름

에이전트 응답 종료 (Stop hook)
  ↓
detect-mid-question.sh:
  마지막 assistant 메시지에서 금지 패턴 검색
  ├─ R2/보안/drift 맥락이 함께 있으면 → 허용 (exit 0)
  └─ 금지 패턴만 단독 감지 → state 파일 생성
  ↓
다음 사용자 입력 (UserPromptSubmit hook)
  ↓
detect-mid-question-reminder.sh:
  state 파일 발견 → additionalContext로 규칙 리마인더 주입
  → state 파일 삭제 (1회성)

금지 패턴 (04-lifecycle "AskUserQuestion 최소화" 위반):

  • 직접 질문: "계속 진행할까요?", "어떻게 할까요?"
  • 선택지 나열: "A/B/C 중", "옵션 중 선택"
  • 대기 선언 (위장된 멈춤): "준비 완료", "명시할 때까지"

허용 맥락 (질문이 정당한 경우):

  • R2 비가역 작업, 보호 브랜치, 인증/결제/권한 변경
  • 범위 drift, force-push, DB 파괴적 변경
  • 명시적 에스컬레이션 ([차단: 포함)

연속 감지 추적: 동일 프로젝트에서 3회+ 연속 감지 시 .claude/logs/hook-events.jsonl에 warning 레벨 기록.

detect-premature-completion 동작 흐름

에이전트 응답 종료 (Stop hook)
  ↓
detect-premature-completion.sh:
  마지막 assistant 메시지에서 완료 선언 패턴 검색
  ├─ 자가검증 마커가 함께 있으면 → 허용 (exit 0)
  └─ 완료 선언만 있고 검증 마커 없음 → state 파일 생성
  ↓
다음 사용자 입력 (UserPromptSubmit hook)
  ↓
detect-premature-completion-reminder.sh:
  state 파일 발견 → additionalContext로 규칙 리마인더 주입
  → state 파일 삭제 (1회성)

완료 선언 패턴: "작업 완료", "완료했습니다", "마무리합니다", "수렴 달성" 등

자가검증 마커 (이것들이 있으면 정당한 완료 — 05-responses 준수):

  • 커밋 해시 ([a-f0-9]{7,})
  • push 대상 보고 (→ origin/feature/...)
  • 변경 파일 목록
  • 체크리스트 항목 ([ ] / [x])
  • HANDOFF 블록 (---HANDOFF---, done:, status:)
  • 신뢰도 레이블 ([검증됨])

idle 모드에서 특히 중요: 사용자 없이 자율 실행 중 에이전트가 검증 없이 "완료"를 선언하면, 실제로는 미완료 상태에서 작업이 멈출 수 있음.

SHOULD-HOOK (효과 큰 선택적 hook)

프로젝트 상황에 따라 추가하면 효과적:

규칙 Hook 파일 Hook 유형 감지 조건 동작
디렉토리 스코프 잠금 guard-freeze.sh PreToolUse (Edit, Write) .claude/freeze-dir.txt에 지정된 디렉토리 밖 파일 수정 시도 차단 (파일 없으면 비활성)
커밋 전 tsc 검증 PreToolUse (Bash) git commit 시도 tsc --noEmit 실행, 실패 시 차단
.ts/.tsx 수정 후 lint PostToolUse (Edit) .ts, .tsx 파일 수정 eslint {file} 자동 실행
.env 수정 경고 PreToolUse (Edit) .env 파일 수정 시도 사용자 확인 요청
cross-push 차단 PreToolUse (Bash) push origin {A}:{B} (A≠B 패턴) 차단

guard-freeze 사용법

작업 범위를 물리적으로 잠그고 싶을 때:

# 잠금 설정 — 이 디렉토리 안의 파일만 수정 허용
echo "/d/dev/my-project/src/components/" > .claude/freeze-dir.txt

# 잠금 해제
rm .claude/freeze-dir.txt

freeze-dir.txt가 없으면 hook은 비활성 (exit 0). governance 채널로 배포한다.

TEXT-ONLY (Advisory로 충분한 규칙)

절차·형식·스타일 관련 규칙은 hook 불필요:

  • 커밋 메시지 형식 (conventional commits)commitlint으로 도구 강제됨 (tooling.commitlint)
  • 응답 신뢰도 레이블 ([검증됨]/[추론]/[모름])
  • 코드 주석 스타일
  • 작업 완료 보고 형식

프로젝트별 Hook 설정 위치

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/guard-branch.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/lint-on-save.sh"
          }
        ]
      }
    ]
  }
}

Hook 스크립트 위치: 각 프로젝트 .claude/hooks/ 디렉토리. 이 파일은 "어떤 것을 hook으로 올려야 하는가"의 판단 기준만 제공. 실제 구현은 각 프로젝트 extension 또는 .claude/hooks/ 참조.


Hook 작성 원칙

  1. 단일 책임: 하나의 hook은 하나의 규칙만 검사
  2. 빠른 실패: 차단 조건이면 즉시 exit 1, 긴 분석 금지
  3. 명확한 메시지: 차단 이유와 대안을 출력에 포함
  4. 멱등성: 동일 입력에 항상 동일 결과
  5. 인젝션 방어: 외부 입력값(파일명, 브랜치명, 커밋 메시지 등)을 hook 스크립트 내에서 직접 eval·$() 실행 금지 — 반드시 변수로 받아 검사만 수행 (2026-02 CVE: 신뢰할 수 없는 리포지토리의 hook이 RCE로 악용된 사례)
#  위험 — 외부 입력 직접 실행
eval "$COMMIT_MSG"
$(git log --format="%s" -1)

#  안전 — 변수로 받아 패턴만 검사
BRANCH=$(git branch --show-current 2>/dev/null)
if [[ "$BRANCH" == "main" ]]; then exit 1; fi
#!/bin/bash
# .claude/hooks/guard-branch.sh 예시

CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
PROTECTED=("main" "master" "develop")

for branch in "${PROTECTED[@]}"; do
  if [ "$CURRENT_BRANCH" = "$branch" ]; then
    echo " 보호 브랜치($branch) 직접 커밋 금지 (01-git 규칙)"
    echo "   feature 브랜치를 생성하세요: git checkout -b feature/$(date +%y%m%d)-{desc}"
    exit 1
  fi
done

Hook Cookbook — 테스트 예제

hook 스크립트가 올바르게 동작하는지 확인하려면 아래 패턴으로 테스트한다. Claude Code hook은 stdin으로 JSON을 받고, exit code로 판정한다.

exit code 규약

exit code 의미
0 통과 (실행 허용)
2 차단 (실행 금지 + 에러 메시지 출력)

guard-push-force.sh 테스트

# 차단되어야 하는 입력
echo '{"tool_name":"Bash","tool_input":{"command":"git push --force origin main"}}' \
  | bash .claude/hooks/guard-push-force.sh
# expected: exit 2, "push --force 금지" 메시지

echo '{"tool_name":"Bash","tool_input":{"command":"git push -f"}}' \
  | bash .claude/hooks/guard-push-force.sh
# expected: exit 2

# 통과해야 하는 입력
echo '{"tool_name":"Bash","tool_input":{"command":"git push origin feature/260404-test"}}' \
  | bash .claude/hooks/guard-push-force.sh
# expected: exit 0

guard-destructive-db.sh 테스트

# 차단
echo '{"tool_name":"Bash","tool_input":{"command":"npx prisma migrate reset"}}' \
  | bash .claude/hooks/guard-destructive-db.sh
# expected: exit 2, "migrate reset 금지" + 수동 절차 안내

echo '{"tool_name":"Bash","tool_input":{"command":"psql -c \"DROP TABLE users\""}}' \
  | bash .claude/hooks/guard-destructive-db.sh
# expected: exit 2

echo '{"tool_name":"Bash","tool_input":{"command":"DELETE FROM orders"}}' \
  | bash .claude/hooks/guard-destructive-db.sh
# expected: exit 2 (WHERE 없는 DELETE)

# 통과
echo '{"tool_name":"Bash","tool_input":{"command":"npx prisma migrate status"}}' \
  | bash .claude/hooks/guard-destructive-db.sh
# expected: exit 0

echo '{"tool_name":"Bash","tool_input":{"command":"npx prisma migrate deploy"}}' \
  | bash .claude/hooks/guard-destructive-db.sh
# expected: exit 0

guard-freeze.sh 테스트

# 잠금 설정
echo "/d/dev/my-project/src/" > .claude/freeze-dir.txt

# 차단 (범위 밖)
echo '{"tool_name":"Edit","tool_input":{"file_path":"/d/dev/my-project/tests/foo.ts"}}' \
  | bash .claude/hooks/guard-freeze.sh
# expected: exit 2, "freeze 범위 밖" 메시지

# 통과 (범위 안)
echo '{"tool_name":"Edit","tool_input":{"file_path":"/d/dev/my-project/src/app.ts"}}' \
  | bash .claude/hooks/guard-freeze.sh
# expected: exit 0

# 비활성 (freeze-dir.txt 없음)
rm .claude/freeze-dir.txt
echo '{"tool_name":"Edit","tool_input":{"file_path":"/anywhere/file.ts"}}' \
  | bash .claude/hooks/guard-freeze.sh
# expected: exit 0

guard-secrets.sh 테스트

# 차단 (시크릿 감지)
# staged 파일에 API_KEY=sk-xxx 가 포함된 상태에서:
echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m \"feat: add api\""}}' \
  | bash .claude/hooks/guard-secrets.sh
# expected: exit 2, 감지된 시크릿 패턴 + 파일 경로 출력

# 통과 (시크릿 없음)
echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m \"docs: update readme\""}}' \
  | bash .claude/hooks/guard-secrets.sh
# expected: exit 0

detect-mid-question.sh 테스트

Stop hook은 transcript_path를 통해 대화 기록을 읽는다. 테스트를 위해 임시 transcript를 생성:

# 테스트 transcript 생성 (금지 패턴 포함)
TMPFILE=$(mktemp)
echo '{"type":"assistant","message":{"content":[{"type":"text","text":"파일 수정 완료했습니다. 계속 진행할까요?"}]}}' > "$TMPFILE"

echo "{\"transcript_path\":\"$TMPFILE\"}" \
  | bash .claude/hooks/detect-mid-question.sh
# expected: exit 0 (Stop hook은 항상 exit 0), .claude/state/mid-question/ 에 state 파일 생성

# R2 맥락이 함께 있으면 무시
echo '{"type":"assistant","message":{"content":[{"type":"text","text":"R2 비가역 작업입니다. 계속 진행할까요? CONFIRM reset-db-20260424"}]}}' > "$TMPFILE"

echo "{\"transcript_path\":\"$TMPFILE\"}" \
  | bash .claude/hooks/detect-mid-question.sh
# expected: exit 0, state 파일 미생성 (허용 맥락)

rm -f "$TMPFILE"

Advisory → Deterministic 전환 판단 기준

다음 질문에 하나라도 "예"이면 hook으로 이중화:

  • "이 규칙을 어기면 되돌릴 수 없는 피해가 생기는가?" (데이터 손실, 보안)
  • "이 규칙이 컨텍스트가 길어지면 무시된 적 있는가?"
  • "사람이 실수로 허용하더라도 절대 실행되면 안 되는가?"

Prompt-type Hook 가이드

Claude Code는 두 가지 hook type을 지원한다:

type 실행 주체 비용 적합 판단
command 셸 스크립트 (bash) 없음 (로컬 실행) 패턴 매칭, 문자열 비교, 파일 존재 확인
prompt LLM (Claude) 토큰 소비 의미 판단, 맥락 이해, 자연어 분석

command vs prompt 선택 기준

판단 기준 command prompt
"이 문자열이 포함되어 있는가?" grep/regex 과잉
"이 파일이 존재하는가?" [ -f ] 과잉
"이 수정이 INTENT 범위 안인가?" 문자열로 판단 불가 의미 비교
"이 커밋 메시지가 적절한가?" commitlint (도구) 과잉
"이 코드 변경이 보안에 영향을 주는가?" 패턴만으로 부족 맥락 필요
"삭제 대상 파일이 다른 곳에서 참조되는가?" ⚠️ grep 가능하나 불완전 import 관계 이해

원칙: command로 해결 가능하면 command를 쓴다. prompt는 의미 판단이 필수인 경우에만 사용한다.

prompt-type hook 설정 예시

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "이 수정이 현재 작업의 INTENT 범위 안에 있는지 판단하라. INTENT.md가 있으면 참조하고, 없으면 사용자의 최근 요청 텍스트를 범위로 사용한다. 범위를 벗어나면 '범위 외 수정입니다: {이유}'를 출력하고 차단하라."
          }
        ]
      }
    ]
  }
}

prompt-type hook 비용 고려

prompt hook은 매 도구 호출마다 LLM 호출을 추가한다:

트리거 빈도 예상 비용 권장
PreToolUse(Bash) — 높음 세션당 수십~수백 회 사용 금지
PreToolUse(Edit) — 중간 세션당 수~수십 회 ⚠️ 신중하게
PostToolUse(Edit) — 중간 세션당 수~수십 회 ⚠️ 신중하게
PreToolUse(Write) — 낮음 세션당 수 회 적합
SessionStart — 극히 낮음 세션당 1회 적합

비용 관리 규칙:

  • 빈번한 도구(Bash, Read)에 prompt hook 금지
  • 동일 판단을 command hook으로 근사할 수 있으면 command 우선
  • prompt hook은 세션당 총 호출 횟수 10회 이하를 목표로 설계

updatedInput — 도구 입력 수정

Claude Code hook은 stdout으로 JSON을 반환하면 도구 입력을 수정할 수 있다. 이를 통해 차단 대신 안전한 형태로 변환하는 패턴이 가능하다.

#!/usr/bin/env bash
# .claude/hooks/safe-rm.sh
# rm -rf를 rm -ri로 변환 (interactive 모드)

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

if echo "$CMD" | grep -qE 'rm\s+-rf'; then
  SAFE_CMD=$(echo "$CMD" | sed 's/rm -rf/rm -ri/g')
  echo "{\"tool_input\":{\"command\":\"$SAFE_CMD\"}}"
  exit 0
fi

주의: updatedInput은 강력하지만 예상치 못한 부작용을 유발할 수 있다. 차단(exit 2) + 안내 메시지가 updatedInput보다 안전하고 투명하다. updatedInput은 아래 조건을 모두 만족할 때만 사용:

  • 변환이 항상 안전한 방향인가?
  • 사용자가 변환 사실을 인지할 수 있는가?
  • 변환 후 명령이 원래 의도를 유지하는가?

prompt hook 사용 시나리오 (ai-rules 기준)

시나리오 트리거 prompt 내용 효과
Drift Detection PreToolUse(Edit) "이 수정이 INTENT 범위 안인가?" G2 게이트 자동화
보안 코드 리뷰 PostToolUse(Edit) "이 변경에 STRIDE 위협이 있는가?" 인증/결제 코드 변경 시 자동 경고
커밋 범위 검증 PreToolUse(Bash) on git commit "이 커밋이 단일 기능 범위인가?" 과대 커밋 방지

현재 ai-rules는 command hook 중심으로 운영한다. prompt hook은 비용이 높아 실험적 용도로만 권장하며, 핵심 가드레일은 반드시 command hook으로 구현한다.


CLI Whitelist Wrapper 패턴

패턴 매칭 hook은 "금지 목록"으로 위험 명령을 차단한다. 반대로 허용 목록(whitelist) 래퍼는 "명시적으로 허용된 서브커맨드만 통과"시킨다.

외부 CLI(gh, aws, docker 등)에서 에이전트가 실행 가능한 명령을 제한할 때 사용한다.

래퍼 예시: gh-safe.sh

#!/usr/bin/env bash
# .claude/wrappers/gh-safe.sh
# 허용된 gh 서브커맨드만 통과
set -euo pipefail

ALLOWED="pr|issue|repo view|run list|run view"
CMD="${1:-}"; shift 2>/dev/null || true

if echo "$CMD" | grep -qE "^($ALLOWED)$"; then
  gh "$CMD" "$@"
else
  echo " gh $CMD 는 허용되지 않은 명령입니다" >&2
  echo "   허용 목록: $ALLOWED" >&2
  exit 2
fi

래퍼 예시: docker-safe.sh

#!/usr/bin/env bash
# .claude/wrappers/docker-safe.sh
set -euo pipefail

ALLOWED="ps|logs|inspect|images|compose up|compose down|compose ps"
CMD="${1:-} ${2:-}"
CMD=$(echo "$CMD" | xargs)  # trim

if echo "$CMD" | grep -qE "^($ALLOWED)$"; then
  docker "$@"
else
  echo " docker $CMD 는 허용되지 않은 명령입니다" >&2
  exit 2
fi

적용 방법

  1. .claude/wrappers/ 디렉토리에 래퍼 스크립트 배치
  2. CLAUDE.md에 에이전트 지시 추가:
    gh 명령은 반드시 .claude/wrappers/gh-safe.sh 를 통해 실행한다.
    직접 gh 호출 금지.
    
  3. 선택적으로 PreToolUse hook에서 직접 gh 호출을 감지하여 차단

래퍼 vs Hook 비교

구분 Hook (차단 목록) Wrapper (허용 목록)
접근 방식 위험한 것 금지 안전한 것만 허용
새 명령 추가 시 기본 허용 (금지 목록에 없으면) 기본 차단 (허용 목록에 없으면)
적합 대상 내부 CLI (git, npm) 외부 CLI (gh, aws, docker)
보안 강도 중간 (알려진 위험만 차단) 높음 (알려진 안전만 허용)