Claude Code 훅 가이드: 편집 후 포맷, 커밋 전 테스트, 폴더 보호 걸기

게시 읽는 시간 약 10분 자동화 워크플로

.env 같은 파일은 Claude가 실수로라도 건드리면 곤란합니다. CLAUDE.md에 “건드리지 마세요”라고 적어 둘 수는 있지만, 그건 어디까지나 부탁이라 대화가 길어지면 잊힐 수 있어요. 그래서 훅으로 아예 잠가 보기로 했습니다.

공식 가이드에 마침 그 용도의 예시 스크립트가 있어서, 그대로 가져다 Windows 11 테스트 환경에 걸었습니다. 그리고 Claude에게 .env를 하나 만들어 달라고 했죠.

그냥 만들어졌습니다. 오류 메시지 하나 없이요.

이 글은 그 원인을 찾아 고친 이야기와, 그 김에 정리한 훅 세 가지(편집 후 포맷, 커밋 전 테스트, 폴더 보호)입니다. 폴더 보호 훅은 2026년 10월 5~6일 Windows 11 · Claude Code 2.1.286 테스트 환경에서 직접 해 본 결과입니다. 공식 문서는 9월 9일 기준입니다.

확인한 버전·환경

  • Claude Code 2.1.286 · Windows 11 · Windows PowerShell 5.1 · Git for Windows 2.53.0 · 확인일 2026-10-06
  • Claude Code 2.1.266 · 확인일 2026-09-09

공식 예시가 Windows에서 아무것도 못 막은 이유

제가 건 스크립트는 공식 예시에 dist/·vendor/만 더한 것입니다. Claude가 Edit·Write로 파일을 건드리기 직전(PreToolUse)에 경로를 보고, 보호 대상이면 exit 2로 막는 구조예요.

.claude/hooks/protect-files.sh:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/" "dist/" "vendor/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

.claude/settings.json에는 이렇게 등록했습니다. macOS·Linux라면 chmod +x로 실행 권한도 줘야 합니다.

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" }] }
    ]
  }
}

화면에는 아무 흔적이 없었고, 이유는 디버그 로그(claude --debug)에만 남아 있었습니다. 훅은 분명히 돌았는데, 결과가 “성공”으로 찍혀 있더라고요.

[DEBUG] "Hook PreToolUse:Write (PreToolUse) success:\nstderr:\n<프로젝트 경로>/.claude/hooks/protect-files.sh: line 3: jq: command not found"

Windows에서 이 훅은 Git Bash로 도는데, Git for Windows에는 jq가 들어 있지 않습니다. jq가 실패하니 FILE_PATH는 빈 값이 되고, 빈 값은 어떤 패턴에도 안 걸리니 스크립트는 그냥 exit 0으로 끝납니다. 훅 입장에선 정상 종료라, 보호 장치가 아무 말 없이 꺼져 있었던 거죠.

에러가 나는 실패는 바로 눈에 띕니다. 이렇게 조용히 통과하는 실패는 사고가 나고 나서야 보입니다. 그래서 보호용 훅은 걸고 나서 일부러 한 번 막혀 보는 것까지 해야 끝난 거라고 생각합니다.

참고로 실행 권한을 따로 주지 않았는데도 Git Bash는 스크립트를 잘 실행했습니다. Windows에서 같은 증상을 만나면 권한보다 jq부터 의심해 보세요.

jq 없이 막는 방법 두 가지

jq를 설치하면 공식 예시가 그대로 돕니다. 그래도 “실행 환경에 jq가 있어야 보호가 된다”는 조건이 붙는 게 영 찜찜해서, 설치 없이 막는 방법을 두 가지 시험해 봤습니다.

둘 다 .env 쓰기를 막았고, Claude에게는 Blocked: <프로젝트 경로>/.env matches protected pattern '.env'가 전달됐습니다. 보호 대상이 아닌 ok.txt는 평소처럼 만들어졌고요.

PowerShell 스크립트를 exec 형태로

Windows라면 저는 이쪽을 권합니다. command에 실행 파일, args에 인자 배열을 주면 셸을 거치지 않아서 따옴표나 경로 문제로 고생할 일이 줄어듭니다.

.claude/hooks/protect-files.ps1:

$data = [Console]::In.ReadToEnd() | ConvertFrom-Json
$path = ([string]$data.tool_input.file_path) -replace '\\', '/'
foreach ($pattern in @('.env', 'package-lock.json', '.git/', 'dist/', 'vendor/')) {
  if ($path -like "*$pattern*") {
    [Console]::Error.WriteLine("Blocked: $path matches protected pattern '$pattern'")
    exit 2
  }
}
exit 0
{ "matcher": "Edit|Write",
  "hooks": [{ "type": "command", "command": "powershell.exe",
    "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ".claude/hooks/protect-files.ps1"] }] }

이 객체를 hooks.PreToolUse 배열에 넣으면 됩니다. ConvertFrom-Json이 경로의 역슬래시를 한 글자로 풀어 주고, -replace '\\', '/'가 그걸 슬래시로 바꿔서 패턴이 맞아떨어집니다.

bash 스크립트에서 jq 줄만 바꾸기

macOS·Linux와 같은 스크립트를 계속 쓰고 싶다면 protect-files.sh의 FILE_PATH 두 줄을 아래 한 줄로 바꾸면 됩니다. JSON 안의 역슬래시는 \\ 두 글자로 들어오니까, 그 둘을 슬래시 하나로 바꾸는 부분까지 들어 있습니다.

FILE_PATH=$(printf '%s' "$INPUT" | sed -n 's/.*"file_path":"\([^"]*\)".*/\1/p' | sed 's#\\\\#/#g')

다만 경로에 큰따옴표가 없다는 가정이 깔려 있어요. 경로에 큰따옴표가 들어갈 수 있는 환경이라면 그냥 jq를 설치하고 공식 예시를 쓰는 편이 안전합니다.

어느 쪽이든 걸고 나서 보호 대상 파일을 하나 만들게 해 보세요. 진짜로 막히는 걸 봐야 마음이 놓입니다.

훅, CLAUDE.md, 권한 규칙은 뭐가 다른가

훅을 처음 볼 때 제일 헷갈리는 게 이겁니다. 같은 규칙을 CLAUDE.md에 쓸지, 권한 규칙에 쓸지, 훅에 걸지.

어디에 하는 일 이럴 때
CLAUDE.md Claude가 참고하는 지시 지키면 좋은 관례
권한 규칙 이 호출을 허용할지 rm -rf 같은 명령 금지
훅 이 시점에 무엇을 실행할지 매번 꼭 돌아야 하는 절차

CLAUDE.md는 컨텍스트가 길어지거나 다른 지시와 부딪히면 건너뛸 수 있습니다. 훅은 조건만 맞으면 모델의 판단 없이 매번 실행되고요.

저는 “어겨지면 사고가 나는가”로 나눕니다. 사고가 나면 훅이나 권한 규칙, 아니면 CLAUDE.md입니다.

셋을 직접 비교해 본 내용은 훅·스킬·CLAUDE.md를 고르는 기준에, 권한 규칙이 어떤 순서로 평가되는지는 Claude Code 권한 설정 글에 따로 썼습니다.

훅이 끼어드는 자리를 두 줄로 보여 주는 그림. 파일을 고칠 때는 Claude가 Edit·Write를 호출하면 PreToolUse가 보호 경로인지 확인해, 맞으면 exit 2로 막고 이유를 Claude에게 전달하며, 아니면 편집이 실행된 뒤 PostToolUse가 포맷터를 실행합니다. 커밋할 때는 Claude가 git commit을 실행하면 PreToolUse가 테스트를 먼저 돌려, 통과하면 커밋이 실행되고 실패하면 exit 2로 막고 실패 로그를 Claude에게 전달합니다.
PreToolUse는 도구가 돌기 전에 막을 수 있고, PostToolUse는 끝난 뒤에 뒷정리를 합니다.

훅은 설정 파일의 hooks 키 아래 이벤트별로 적고, 어느 파일에 적느냐가 범위를 정합니다. 나만 쓸 훅은 ~/.claude/settings.json(모든 프로젝트)이나 .claude/settings.local.json(이 프로젝트, git 무시)에, 팀이 같이 지킬 훅은 .claude/settings.json에 커밋합니다.

잘 등록됐는지는 세션 안에서 /hooks로 볼 수 있어요. 읽기 전용이라 고칠 때는 설정 파일을 직접 열어야 합니다.

문서를 읽다가 한 번 더 걸린 게 matcher였습니다. 이벤트마다 맞대는 대상이 다르거든요.

PreToolUse·PostToolUse에서는 도구 이름(Bash, Edit|Write, mcp__github__.*)이고, SessionStart에서는 시작 방식(startup, resume, compact)입니다. 빈 문자열이나 *, 아예 생략하면 “언제나”고요.

도구 이름보다 더 좁히고 싶으면 권한 규칙과 같은 문법의 if 필드를 씁니다. 그런데 if는 최선을 다해 거르는 정도라서, $(...)나 변수로 가려진 명령에는 그냥 훅을 실행해 버립니다. 정말 막아야 하는 명령이라면 if에 맡기지 말고 권한 규칙으로 막으세요.

편집 후 자동 포맷

Claude가 파일을 고친 직후 포맷터를 돌리는 훅입니다. 공식 가이드의 예시를 한 줄로 줄였어요.

{
  "hooks": {
    "PostToolUse": [
      { "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }] }
    ]
  }
}

눈치채셨겠지만 이 예시도 jq에 기댑니다. Windows에서 쓰려면 jq부터 설치해야 해요.

이 훅은 성공해도 화면에 아무것도 안 나옵니다. 걸렸는지 보려면 Claude에게 작은따옴표를 쓴 JS 파일을 고치게 하고, 파일이 Prettier 기본값대로 큰따옴표로 바뀌었는지 보면 됩니다.

미리 알아 두면 좋은 게 두 가지 있습니다. PostToolUse는 도구가 끝난 뒤에 돌기 때문에 포맷터가 실패해도 편집은 이미 끝난 상태예요.

그리고 Claude가 Bash 명령으로 파일을 고치면 이 매처에 안 걸립니다. 그것까지 잡고 싶다면 매처에 Bash|PowerShell을 더하거나, 디스크의 파일이 바뀔 때마다 도는 FileChanged 이벤트를 쓰면 됩니다.

커밋 전에 테스트 강제하기

긴 세션 끝에 Claude가 테스트를 건너뛰고 커밋해 버리는 걸 막는 훅입니다. 공식 문서의 형태를 그대로 따랐습니다.

.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [{ "type": "command", "if": "Bash(git commit *)", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-before-commit.sh" }] }
    ]
  }
}

.claude/hooks/check-before-commit.sh:

#!/bin/bash
if ! npm test --silent; then
  echo "테스트 실패: 통과할 때까지 커밋할 수 없습니다" >&2
  exit 2
fi
exit 0

if 덕분에 git status 같은 다른 명령에는 훅이 돌지 않습니다. npm test && git commit -m "..."처럼 이어 붙인 명령도 하위 명령 단위로 검사되니 훅이 제대로 걸리고요.

주의: Windows의 Claude Code는 셸 명령을 PowerShell 도구로 실행하기도 하는데, 그때 매처가 Bash뿐인 훅은 발동하지 않습니다.

Windows에서는 그래서 매처를 Bash|PowerShell로 넓혀 둡니다. if 규칙 하나는 도구 하나에만 맞기 때문에, 도구마다 핸들러를 따로 둬야 해요.

공식 문서의 Bash|PowerShell 예시를 커밋 훅에 옮겨 온 것입니다. 매처가 Bash뿐일 때 정말 안 걸리는지 확인한 기록은 훅이 작동하지 않을 때 점검 순서에 있습니다.

{ "matcher": "Bash|PowerShell",
  "hooks": [
    { "type": "command", "if": "Bash(git commit *)", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-before-commit.sh" },
    { "type": "command", "if": "PowerShell(git commit *)", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-before-commit.sh" }
  ] }

커밋을 막는 대신 “테스트가 통과할 때까지 Claude가 일을 못 끝내게” 하고 싶다면 Stop 이벤트가 있습니다. Claude가 응답을 마칠 때마다 발동하고, 훅이 막으면 대화가 Claude에게 다시 넘어가요. 다만 잘못 짜면 무한 루프에 빠지기 쉬워서, 바로 아래 사례를 꼭 보고 쓰세요. 테스트를 Stop 훅에 걸었을 때 범위를 좁힌 요청과 부딪혀 24번 막힌 실측은 Claude Code가 스스로 테스트하게 하는 법에 따로 적었습니다.

다른 사람들이 겪은 문제들

제 테스트 환경에서 겪은 건 위의 jq 문제 하나였습니다. 아래는 GitHub 이슈에 올라온 사례들이에요. 첫 번째는 제 환경에서 재현되지 않았습니다.

Windows에서 PreToolUse·PostToolUse가 등록은 되는데 발동하지 않음

/hooks에는 보이고 Linux에서는 잘 도는 설정이 Windows(Git Bash)에서는 실행되지 않는다는 보고입니다. UserPromptSubmit 같은 세션 단위 훅은 됐다고 하고요(Dany-Axhor, 2026-02-15, #25981, Claude Code 2.1.42).

유지보수자 답변이 없어서 원인은 확정되지 않았습니다.

제가 Windows 11 · Claude Code 2.1.286에서 해 봤을 때는 PreToolUse(셸·exec 형태 모두)와 PostToolUse가 다 발동했습니다. 같은 증상이라면 claude --debug hooks로 훅이 시도되는지부터 보고, 그래도 안 되면 args 배열을 쓰는 exec 형태로 바꿔 보세요.

PostToolUse 포맷터가 성공했다는데 파일에 반영되지 않음

훅이 파일을 고쳤다는 로그는 남는데 디스크에서는 변경이 사라진다는 보고입니다. Prettier·RuboCop·Black·ESLint --fix 전반에서 나왔어요(panozzaj, 2025-10-20, #10011, Claude Code 2.0.24, macOS).

원인은 확정되지 않았고, Write 도구가 훅이 끝난 뒤 파일을 다시 써서 덮는다는 추정만 있습니다. 같은 일을 겪는다면 훅 끝에서 파일을 다시 읽어 반영됐는지 확인하고, 최신 버전에서 다시 보세요.

Windows에서 훅이 5분 넘게 멈추거나 세션이 죽음

첫 메시지에서는 되던 훅이 두 번째부터 멈췄다는 보고입니다. PostToolUse로 ruff 포맷, PreToolUse로 커밋·푸시 전 pytest를 건 설정이었어요(StrategicMilk, 2026-03-14, #34457, Claude Code 2.1.73, Windows 11 Git Bash/MSYS2).

원인은 확정되지 않았지만, 저는 설정의 timeout이 눈에 걸렸습니다. 15000과 300000이 들어 있는데 timeout은 초 단위라서 각각 약 4시간, 약 83시간입니다.

밀리초로 착각했을 수 있다는 건 이슈에 없는 제 추정입니다. 훅에 timeout을 넣는다면 초 단위라는 것만은 꼭 기억해 두세요.

Stop 훅이 무한 루프에 빠짐

Stop 훅이 {"ok": false}를 반복하면서 약 50분, 100회 넘게 멈추지 않았다는 보고입니다. 비동기 서브에이전트를 기다리던 중이었다고 해요(AndresMuelas2004, 2026-05-03, #55754, Windows/PowerShell, 중복으로 닫힘).

스크립트가 stop_hook_active를 확인하지 않은 게 문제였습니다.

Claude Code는 Stop 훅이 진전 없이 8번 연속 막으면 그 훅을 무시하는데, 이슈의 100회 이상은 이 상한과 맞지 않습니다. 상한이 생기기 전 버전 조합이었을 수도 있습니다.

Stop 훅을 쓴다면 첫머리에서 stop_hook_active부터 확인하는 걸 잊지 마세요. 정말로 8번보다 많이 반복해야 한다면 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP으로 상한을 올릴 수 있습니다.

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
# 나머지 검증 로직

훅이 아예 아무 일도 안 한다면, 훅이 등록·매칭·실행·해석 중 어디서 멈췄는지 가르는 순서를 따라가 보세요.

마치며

훅은 설정 몇 줄이면 걸리지만, 건다고 끝이 아니라는 걸 이번에 제대로 배웠습니다. 특히 보호용 훅이 조용히 통과해 버리면 안 건 것보다 오히려 위험합니다. 막혀 있다고 믿게 되니까요.

그래서 새 훅을 걸면 일부러 한 번 걸려 보는 것까지를 설정의 일부로 생각하시길 권합니다. 보호하고 싶은 파일 하나를 정해 위 훅을 걸고, Claude에게 그 파일을 만들게 해 보세요. 훅을 어디서부터 들여야 할지 전체 그림이 궁금하다면 반복 작업을 에이전트에 넘기는 네 단계도 함께 읽어 보시면 좋습니다.

참고한 공식 자료

모두 2026년 9월 9일에 확인했습니다.

인용한 공개 사례

변경 이력

  • 2026-10-06 — Windows 11에서 직접 확인한 결과를 더했습니다. 공식 예시의 폴더 보호 스크립트는 Git for Windows에 jq가 없어 아무것도 막지 못했고, jq 없이 막는 PowerShell 훅과 bash 훅을 각각 확인해 실었습니다. PreToolUse·PostToolUse가 Windows에서 발동하지 않는다는 공개 보고는 이 환경에서 재현되지 않았습니다.