Claude Code로 작업하다 보면 저장할 때마다 포맷 돌리고, rm -rf 같은 위험 명령은 없나 눈으로 확인하고, .env는 건드리지 말라고 매번 프롬프트에 적게 됩니다. 사람이 매번 챙기면 언젠가 한 번은 까먹고, 그 한 번이 규칙 붕괴나 사고로 이어집니다. AI 도구를 실무에 붙여 돌려온 관점에서 보면, "부탁하는 규칙"보다 "항상 실행되는 규칙"이 훨씬 안전합니다. 이 순서대로 가면 settings.json 몇 줄로 포맷·위험 명령 차단·파일 보호·알림·컨텍스트 재주입 5가지가 자동으로 걸립니다.

1. Hooks가 뭐고 왜 쓰나
Claude Code Hooks는 Claude Code 생명주기의 특정 시점에 사용자가 정의한 셸 명령을 자동 실행하는 기능입니다. 핵심은 실행 방식입니다. CLAUDE.md에 "커밋 전 테스트 돌려줘"라고 적는 건 LLM이 그때그때 "할지 말지" 판단하는 부탁입니다. 반면 Hooks는 LLM 판단을 거치지 않고 항상 실행되는 결정론적(deterministic) 제어입니다. 프로젝트 규칙 강제(포맷·린트), 반복 작업 자동화(알림·로깅), 기존 도구 연동(prettier·eslint)에 쓰면 "이번엔 왜 포맷을 안 돌렸지?" 같은 편차가 사라집니다.
한 가지 성질만 짚으면, PreToolUse 훅의 deny는 권한 모드보다 먼저 걸려 bypassPermissions에서도 차단됩니다. 반대로 훅의 allow는 settings.json의 deny 규칙을 못 넘습니다. 훅은 규칙을 조일 순 있어도 풀 수는 없습니다. 정책 강제용으로 안전한 이유입니다.
2. 설정 위치와 구조
Hooks는 settings.json에 넣습니다. 어느 파일에 넣느냐가 적용 범위를 정합니다.
| 위치 | 범위 | 팀 공유 |
|---|---|---|
~/.claude/settings.json |
모든 프로젝트(내 PC) | 안 됨 |
.claude/settings.json |
단일 프로젝트 | 됨(레포 커밋) |
.claude/settings.local.json |
단일 프로젝트 | 안 됨(gitignore) |
| 관리형 정책(managed policy) | 조직 전체 | 관리자가 배포 |
팀 전체가 지켜야 할 규칙은 .claude/settings.json에 넣어 커밋하고, 내 PC에서만 쓰는 알림은 settings.local.json에 넣어 gitignore로 뺍니다. 조직 차원 강제는 관리형 정책으로 내려보냅니다.
구조는 가장 바깥의 hooks 객체 아래에 이벤트명을 키로 두는 형태입니다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "..." }
]
}
]
}
}
matcher는 어떤 도구에 걸지 지정합니다. 값은 도구명(Bash, Edit, Write)이고 정규식이 됩니다(Edit|Write, mcp__.*). matcher를 비우면 해당 이벤트마다 예외 없이 발동합니다. 등록이 됐는지 확실치 않으면 /hooks 명령으로 확인합니다. /hooks는 이벤트별 등록 훅을 보여주는 읽기 전용 브라우저라 추가·수정은 settings.json을 직접 편집합니다.
이 글에서 쓰는 이벤트는 5개입니다. PreToolUse(도구 실행 전, 차단 가능), PostToolUse(도구 성공 후, 되돌리기 불가), Notification(입력·승인 대기 알림), SessionStart(세션 시작·재개), Stop(응답 종료 시). 입력은 stdin으로 JSON(tool_name·tool_input·cwd 등)이 들어오고 jq로 파싱합니다.
3. 실전 훅 5가지
아래 5개는 settings.json에 그대로 붙여 쓸 수 있는 형태입니다. exit code 규칙만 먼저 기억하면 됩니다. exit 0은 이의 없음, exit 2는 차단이고 stderr가 Claude에게 피드백으로 갑니다. 그 외 코드는 진행하되 오류로 표시됩니다.

① 저장할 때마다 자동 포맷
PostToolUse + matcher Edit|Write. 편집이 끝난 파일 경로를 jq로 뽑아 prettier에 넘깁니다.
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
포맷은 파일이 이미 저장된 뒤에 손보는 작업이라 PostToolUse가 맞습니다. 매번 "포맷 돌려줘"라고 시킬 필요가 없어집니다.
② 위험한 Bash 명령 차단
PreToolUse + matcher Bash. 스크립트가 stdin으로 명령을 읽어 rm -rf나 drop table이 보이면 stderr로 이유를 찍고 exit 2로 막습니다.
#!/bin/bash
# .claude/hooks/block-rm.sh
cmd=$(jq -r '.tool_input.command')
if echo "$cmd" | grep -qiE 'rm -rf|drop table'; then
echo "위험 명령 차단: $cmd" >&2
exit 2
fi
차단은 명령이 실행되기 전에 잡아야 의미가 있습니다. 그래서 PreToolUse입니다. 여기서 흔한 삽질 하나. 처음 이걸 붙일 때 PostToolUse에 걸어놓고 왜 안 막히나 한참 봤습니다. PostToolUse는 도구가 이미 성공한 뒤라 명령은 이미 실행된 상태입니다. 되돌릴 방법이 없습니다. 차단은 반드시 PreToolUse입니다. 앞서 말한 것처럼 이 deny는 bypassPermissions에서도 먹습니다.
③ 민감 파일 보호
PreToolUse + matcher Edit|Write. 편집 대상 경로가 .env·package-lock.json·.git/이면 막습니다.
#!/bin/bash
# .claude/hooks/protect-files.sh
path=$(jq -r '.tool_input.file_path')
if echo "$path" | grep -qE '\.env|package-lock\.json|\.git/'; then
echo "보호 파일 편집 차단: $path" >&2
exit 2
fi
macOS·Linux에서는 스크립트에 chmod +x 실행권한을 줘야 합니다. 안 주면 훅이 조용히 안 뜹니다.
④ 작업 끝나면 데스크톱 알림
Notification + matcher ""(빈 값이라 모든 알림에 발동). 승인 대기나 입력 대기 상황을 놓치지 않게 알림을 띄웁니다. Windows PowerShell 기준입니다.
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""
}
]
}
macOS는 osascript, Linux는 notify-send로 같은 걸 합니다. 긴 작업 걸어두고 다른 창 보다가 승인 프롬프트를 놓치는 일이 사라집니다.
⑤ 컴팩션 후 컨텍스트 재주입
SessionStart + matcher compact. 컨텍스트가 압축되면 프로젝트 규칙이 흐려집니다. 이때 stdout으로 규칙을 다시 흘려 넣습니다. SessionStart 훅의 stdout은 컨텍스트로 주입됩니다.
{
"matcher": "compact",
"hooks": [
{ "type": "command", "command": "echo '규칙: npm 대신 Bun. 커밋 전 bun test.'" }
]
}
echo 대신 git log --oneline -5 같은 동적 출력도 넣을 수 있습니다. 재개(resume)나 세션 시작(startup)에 최근 커밋을 물려주면 Claude가 맥락을 빨리 잡습니다.
4. 자주 막히는 지점
훅은 안 될 때 조용히 안 되는 경우가 많아 원인 짚기가 까다롭습니다. 자주 보는 두 가지만 정리합니다.
증상: 훅을 등록했는데 아무 반응이 없다.
원인: matcher 대소문자 불일치(대소문자를 구분합니다), jq 미설치, 스크립트 실행권한 없음 중 하나인 경우가 대부분입니다.
해결: /hooks로 등록 상태부터 확인하고, jq를 설치하고, macOS·Linux면 스크립트에 chmod +x를 줍니다.
증상: Stop 훅이 무한히 반복해서 돈다.
원인: Stop 훅이 매번 작업을 막으면 Claude가 다시 응답하고 또 훅이 막는 루프가 생깁니다.
해결: 입력 JSON의 stop_hook_active가 true면 exit 0으로 조기 종료합니다. 참고로 8회 연속 차단되면 Claude Code가 그 훅을 무시합니다.
한 가지 더. 구조화 제어를 쓸 때 exit 2와 JSON 출력을 섞지 마세요. exit 2로 나가면 stdout의 JSON은 무시됩니다. 세밀한 제어(allow/deny/ask)가 필요하면 exit 0 + stdout에 JSON을 씁니다. PreToolUse는 hookSpecificOutput.permissionDecision, PostToolUse·Stop은 가장 바깥 레벨의 decision: "block" 형태입니다.
Q&A — 자주 보는 질문 4개
훅이 안 먹혀요.
십중팔구 matcher 대소문자, jq 미설치, 스크립트 실행권한 셋 중 하나입니다. /hooks로 등록부터 확인하세요. matcher를 비우면 해당 이벤트 전체에 걸린다는 점도 헷갈리기 쉽습니다.
Windows에서도 되나요.
됩니다. 알림은 위 PowerShell 명령을 쓰고, 셸 스크립트가 필요한 훅은 Git Bash나 WSL 환경에서 실행하면 됩니다. Git Bash 프로필이 조건 없이 echo를 찍으면 JSON 앞에 섞여 파싱이 깨지니, 프로필 echo는 if [[ $- == *i* ]]로 감싸 대화형일 때만 나오게 합니다.
팀과 공유하려면요.
공유할 훅은 .claude/settings.json에 넣어 레포에 커밋합니다. 내 PC에서만 쓸 알림 같은 건 .claude/settings.local.json에 넣고 gitignore로 뺍니다. 조직 전체 강제는 관리형 정책으로 배포합니다.
prompt 훅은 뭔가요.
훅 타입은 command(기본, 셸) 외에 http·mcp_tool·prompt 등이 있습니다. prompt는 Haiku 같은 LLM에 판단을 맡기는 타입입니다. 단순 규칙 강제는 command로 결정론적으로 처리하는 게 예측 가능하고, 애매한 판단이 필요할 때만 prompt를 고려합니다.
마무리
Hooks의 핵심은 "부탁"을 "규칙"으로 바꾸는 겁니다. 포맷·차단·보호처럼 매번 챙겨야 하는 건 PreToolUse·PostToolUse로 강제하고, 알림·컨텍스트 주입 같은 편의는 Notification·SessionStart로 붙입니다. 차단이 필요하면 PreToolUse + exit 2, 이것 하나만 기억해도 사고 대부분은 막힙니다. 같은 환경이면 위 5개 config를 settings.json에 그대로 붙여도 무리 없습니다.
설치 환경: Windows 11, Node.js v24, Claude Code
'AI 활용법 > Claude 시리즈' 카테고리의 다른 글
| MCP 서버 직접 만들기 — Claude Code에 내 도구 붙이기 (0) | 2026.07.16 |
|---|---|
| Claude Code에 MCP 서버 연동하기 — 노션·GitHub·Sentry 붙이는 법 (0) | 2026.07.10 |
| Claude Sonnet 5 vs Opus 4.8 — 기본 모델 바뀌었는데 Opus 계속 써도 되나 (0) | 2026.07.07 |
| Claude Code 서브에이전트 실전 배치 — 역할 분리부터 병렬 3~5개 스윗스팟까지 (0) | 2026.07.06 |
| CLAUDE.md 작성법 — 카파시 65줄을 프로젝트에 맞게 변형하는 5단계 (0) | 2026.06.16 |
IT 기술과 개발 내용을 포스팅하는 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!