본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 24. 23:32

Claude Code에게 규칙을 말하지 마세요. 훅(Hooks)으로 강제하세요.

요약

Claude Code의 CLAUDE.md 규칙 준수 한계를 극복하기 위해 훅(Hooks)을 활용하는 방법을 제안합니다. 훅을 통해 Node 버전, Git 브랜치 보호, 보안 스캔, 코드 포맷팅 등을 강제하여 Claude의 주의력 저하 문제를 해결할 수 있습니다.

핵심 포인트

  • CLAUDE.md는 Claude의 주의력이 흐트러지면 규칙을 놓칠 수 있음
  • 훅(Hooks)은 PreToolUse, PostToolUse 시점에 실행되는 쉘 스크립트임
  • 훅은 Claude가 거부할 수 없는 강제적인 실행 메커니즘을 제공함
  • 실패 시 Claude가 stderr를 읽고 스스로 수정하여 재시도함

저는 CLAUDE.md에 규칙을 작성하는 것을 그만두었습니다. CLAUDE.md는 Claude Code의 프로젝트 메모리 파일로, 컨벤션(conventions), 빌드 명령(build commands), 그리고 모든 세션에 적용되어야 하는 "항상 X를 수행할 것"과 같은 규칙들을 저장하는 곳입니다. 제 파일에는 Node 버전에 관한 네 줄의 규칙이 있었습니다. Claude는 이를 따랐습니다. 대부분의 경우에는요. 문제는 나머지 경우였습니다. 긴 작업이 진행됨에 따라 Claude의 주의력(attention)은 흐트러집니다. 세션 시작 시 로드된 규칙은 2시간 뒤에 수행하는 작업에 영향을 주지 못할 수도 있습니다. 특정 작업에 집중하기 위해 생성된 서브 에이전트(subagent)는 프로젝트의 CLAUDE.md를 컨텍스트(context)에 포함하지 않을 수도 있습니다. 형제 리포지토리(sibling repo)의 새로운 워크트리(worktree)에는 해당 규칙이 아예 없을 수도 있습니다. 이 중 어떤 상황이라도 발생하면, pnpm build는 PATH에 설정된 어떤 Node 버전(제 경우 20이 아닌 22)으로도 조용히 실행됩니다. 빌드는 성공합니다. 하지만 런타임(runtime)이 잘못되었습니다. 배포할 때까지 에러가 발생하지 않습니다. 저는 당연한 해결책을 시도했습니다. 규칙을 더 많은 곳에 추가하는 것이었습니다. 사용자 범위(User-scope)의 CLAUDE.md, 시스템 프롬프트(System prompt), 모든 프롬프트에 추가하기 등입니다. 준수율은 향상되었습니다. 하지만 실패 모드(failure mode)는 결코 사라지지 않았습니다. 단지 더 드물게 발생하고 포착하기 어려워졌을 뿐입니다. 그때 깨달았습니다. Claude에게 규칙을 따르라고 요청하는 것은, Claude가 그것을 반드시 수행하도록 보장하는 것과는 다르다는 사실을 말입니다. 더 많은 리마인더(reminders)를 준다고 해서 그 간극이 메워지지는 않을 것입니다. CLAUDE.md는 Claude에게 무엇을 할지 알려줍니다. 훅(hook)은 Claude가 그것을 반드시 하도록 보장합니다.

요약(TL;DR): 저는 이제 모든 Claude Code 세션에서 다음 네 가지 훅(각각 약 30줄의 bash 스크립트)을 실행합니다:

훅(Hook)강제하는 내용
1. nvm-guard모든 Node 명령은 Node 20을 사용해야 함
2. main-guardmain/master 브랜치로 git push 금지
3. secret-scan작성된 파일에 Stripe/AWS/GitHub 키 포함 금지
4. auto-formatClaude가 수정하는 모든 파일은 포맷팅(formatted)됨

아래에 네 가지 훅에 대한 코드가 있습니다.

훅(Hooks)이란 무엇인가?
훅은 Claude Code가 정해진 라이프사이클(lifecycle) 시점에 실행하는 쉘 스크립트(shell scripts)입니다: 도구 호출 전(PreToolUse), 도구 호출 후(PostToolUse), 세션 종료 시(Stop). 흐름은 다음과 같습니다:

Claude가 무언가를 실행하려고 함 ↓ 훅이 곧 일어날 일에 대한 JSON을 수신 ↓ 훅이 결정: exit 0 → 허용, exit 2 → 차단 + stderr 메시지를 Claude에게 반환

훅이 차단하면, Claude는 stderr를 읽고 스스로 수정한 뒤 재시도합니다. 당신이 개입할 필요가 없습니다. Claude는 훅을 잊을 수 없습니다.

Claude는 그 규칙들을 거부할 수 없습니다. 훅(Hooks)은 그냥 실행될 뿐입니다.

훅 1: Node 20 강제하기

#!/usr/bin/env bash
# ~/.claude/hooks/nvm-guard.sh
set -euo pipefail

input = " $( cat ) "
tool_name = " $( printf '%s' " $input " | jq -r '.tool_name // ""' ) "
command = " $( printf '%s' " $input " | jq -r '.tool_input.command // ""' ) "

[[ " $tool_name " == "Bash" ]] || exit 0
[[ -n " $command " ]] || exit 0

# 명령 경계에서 node 기반 명령어를 매칭합니다.
if ! printf '%s' " $command " | grep -qE '\ '(^|[;&|(]|&&|\|\|)[[:space:]]*(pnpm|npm|yarn|bun|npx|node)([[:space:]]|$)' ; then
    exit 0
fi

# 동일한 호출 내에서 nvm use가 이미 실행된 경우 허용합니다.
if printf '%s' " $command " | grep -qE 'nvm[[:space:]]+use' ; then
    exit 0
;
fi

if printf '%s' " $command " | grep -qE '^[[:space:]]*nvm[[:space:]]' ; then
    exit 0
;
fi

cat > &2 << ' EOF '
`nvm use 20` 없이 Node 기반 명령어가 감지되었습니다. 다음과 같이 단일 Bash 호출로 다시 실행하세요:
source ~/.nvm/nvm.sh && nvm use 20 && <원래의 명령어>
EOF

exit 2

차단 후 재시도(block-then-retry)의 실제 동작 모습:

● Bash(pnpm run dev)
⎿ PreToolUse:Bash 훅에 의해 차단됨. 다음과 같이 다시 실행하세요: source ~/.nvm/nvm.sh && nvm use 20 && <명령어>
● Bash(source ~/.nvm/nvm.sh && nvm use 20 && pnpm run dev)
⎿ 이제 node v20.20.2를 사용 중입니다.
▲ Next.js 15.0.0

2초 정도 더 느려졌지만, 잘못된 버전으로 빌드되는 미스터리는 사라졌습니다.

훅 2: main 브랜치로의 push 차단

우리 모두 Claude가 git push origin main을 타이핑하는 것을 보며 등골이 서늘해지는 경험을 한 적이 있습니다. 간단하게 방지할 수 있습니다.

#!/usr/bin/env bash
# ~/.claude/hooks/main-guard.sh
set -euo pipefail

input = " $( cat ) "
command = " $( printf '%s' " $input " | jq -r '.tool_input.command // ""' ) "

if printf '%s' " $command " | grep -qE '\ 'git[[:space:]]+push.*[[:space:]](main|master)([[:space:]]|$|:)' ; then
    if [[ " ${ CLAUDE_ALLOW_MAIN_PUSH :-} " != "1" ]] ; then
        cat > &2 << ' EOF '
main/master로의 push가 차단되었습니다. 기능 브랜치(feature branch)를 생성하여 PR을 보내거나, CLAUDE_ALLOW_MAIN_PUSH=1과 함께 다시 실행하세요.
EOF
        exit 2
    fi
fi

exit 0

탈출구 역할을 하는 환경 변수(env var)가 중요합니다. 너무 깔끔하게 차단만 하면 Claude는 창의적인 우회 방법을 만들어낼 것입니다.

탈출구를 문서화하면 Claude는 즉흥적인 행동을 멈춥니다. 훅(Hook) 3: 비밀 정보(Secrets) 작성을 거부하기. 이 방법은 저를 두 번이나 구했습니다. 한 번은 Claude가 JSDoc 예시 안에 실제 STRIPE_SECRET_KEY를 붙여넣은 적이 있었습니다. 디스크에 기록되기 전에 잡아냈죠.

#!/usr/bin/env bash
# ~/.claude/hooks/secret-scan.sh
set -euo pipefail

input="$(cat)"
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // ""')"
content="$(printf '%s' "$input" | jq -r '.tool_input.content // .tool_input.new_string // ""')"

[[ "$tool_name" =~ ^(Write|Edit)$ ]] || exit 0
[[ -n "$content" ]] || exit 0

if printf '%s' "$content" | grep -qE \
  -e 'sk_(live|test)_[A-Za-z0-9]{20,}' \
  -e 'AKIA[0-9A-Z]{16}' \
  -e 'ghp_[A-Za-z0-9]{36}' \
  -e 'xox[baprs]-[A-Za-z0-9-]{10,}' \
  -e '-----BEGIN [A-Z ]+ PRIVATE KEY-----' ; then
  cat >&2 << 'EOF'
Possible secret detected. Write blocked. Use sk_test_fake_replace_me or move the real value to .env (gitignored).
EOF
  exit 2
fi

exit 0

사용 중인 스택에서 사용하는 패턴들, 즉 내부 API 형식, OpenAI 키, 또는 식별 가능한 모든 것들에 대해 패턴을 추가하세요.

훅(Hook) 4: 모든 수정 시 포맷팅(Format) 수행. 조용한 PostToolUse. Claude가 글을 쓰면, 다음 읽기 작업이 일어나기 전에 파일이 포맷팅됩니다. 더 이상 공백 노이즈로 인한 디프(diff) 차이가 발생하지 않습니다.

#!/usr/bin/env bash
# ~/.claude/hooks/auto-format.sh
set -euo pipefail

file="$(jq -r '.tool_input.file_path // ""')"

[[ -n "$file" && -f "$file" ]] || exit 0

case "$file" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.md|*.css)
    command -v prettier > /dev/null && prettier --write "$file" --log-level=silent || true
    ;;
  *.go)
    command -v gofmt > /dev/null && gofmt -w "$file" || true
    ;;
  *.py)
    command -v ruff > /dev/null && ruff format "$file" --quiet || true
    ;;
  *.rs)
    command -v rustfmt > /dev/null && rustfmt "$file" --quiet || true
    ;;
esac

exit 0

포맷터(Formatter)가 없나요? || true를 붙여두면 조용히 아무 작업도 하지 않고 넘어갑니다. Claude는 포맷팅이 일어났다는 사실조차 모르게 됩니다.

각 스크립트를 ~/.claude/hooks/에 저장하고 chmod +x ~/.claude/hooks/.sh를 실행하세요. 그리고 ~/.claude/settings.json에 다음을 추가하세요: "hooks" : { "PreToolUse" : [ { "matcher" : "Bash" , "hooks" : [ { "type" : "command" , "command" : "/Users/YOU/.claude/hooks/nvm-guard.sh" }, { "type" : "command" , "command" : "/Users/YOU/.claude/hooks/main-guard.sh" } ] }, { "matcher" : "Write|Edit" , "hooks" : [{ "type" : "command" , "command" : "/Users/YOU/.claude/hooks/secret-scan.sh" }] } ], "PostToolUse" : [ { "matcher" : "Write|Edit" , "hooks" : [{ "type" : "command" , "command" : "/Users/YOU/.claude/hooks/auto-format.sh" }] } ] } } 새로운 Claude 세션을 시작하세요. /hooks를 실행하여 로드되었는지 확인하세요. 알아야 할 세 가지가 있습니다: matcher는 도구 이름에 대한 정규 표현식(regex)입니다. Bash, Write|Edit, . 모두 작동합니다. 경로는 반드시 절대 경로여야 합니다. settings.json 내부에서는 가 확장되지 않습니다. 하나의 matcher 아래 여러 개의 훅은 순서대로 실행됩니다. 첫 번째에서 exit 2를 하면 나머지는 중단됩니다. 만들 가치가 있는 다섯 가지 추가 기능이 있습니다: 테스트 게이트(Test gate): PostToolUse는 모든 파일 편집에 대해 관련 테스트를 실행합니다. 빨간색 결과가 나오면 exit 2를 합니다. Diff 제한(Diff cap): PreToolUse는 N 라인 이상의 단일 Write/Edit을 차단하여 작고 검토 가능한 변경을 강제합니다. 의존성 잠금(Dependency lock): 허용 목록에 <pkg>가 없는 한 pnpm add <pkg>를 차단합니다. 비용 경고(Cost warn): 세션에서 $X보다 많은 금액이 소모되면 훅이 알림을 보냅니다. 금지 플래그 가드(Forbidden flag guard): --no-verify, --force, --dangerously-skip-permissions 사용을 차단합니다. 각각 2030줄의 bash 코드로 구성됩니다. 이 각각은 여러분이 더 이상 생각할 필요가 없게 만드는 실수 유형을 제거해 줍니다. 운영 매뉴얼에

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0