Claude Code Hooks 실전 가이드 — PreToolUse·PostToolUse로 나만의 자동화 파이프라인 구축하기
AI 코딩 도구를 쓰다 보면 한 번쯤 이런 경험을 하게 됩니다. "분명 .env 건들지 말라고 했는데…" 혹은 "포매팅 돌려달라고 했더니 이번엔 잊었네." 프롬프트로 아무리 정교하게 지시해도, LLM은 기억하지 못합니다. 매 턴마다 새로운 존재니까요.
Claude Code Hooks는 바로 그 지점을 파고드는 기능입니다. Claude가 어떤 도구를 실행하기 전후에 내가 원하는 스크립트를 반드시 실행하도록 시스템이 강제합니다. 프롬프트 지시와 달리, 훅은 결정론적으로 동작합니다. Claude가 무언가를 잊어도, 훅은 절대 잊지 않습니다.
이 글을 읽고 나면 PreToolUse로 위험한 작업을 사전에 차단하고, PostToolUse로 편집 즉시 린팅·타입 체크·시크릿 스캔을 자동화하는 파이프라인을 직접 구성할 수 있게 됩니다. 실제로 핵심 스크립트는 코드 20줄 미만입니다.
핵심 개념
훅이란 무엇이고, 왜 필요한가
Claude Code는 내부적으로 "에이전트 루프"를 돌립니다. 사용자 요청을 받고 → 어떤 도구를 쓸지 결정하고 → 도구를 실행하고 → 결과를 보고 → 다음 행동을 결정하는 사이클이죠. 훅은 이 루프의 특정 시점에 끼어들 수 있는 이벤트 핸들러입니다.
| 이벤트 | 실행 시점 | 할 수 있는 것 |
|---|---|---|
| PreToolUse | 도구 파라미터 생성 후, 실제 실행 전 | decision 필드로 approve / block 결정 |
| PostToolUse | 도구 성공적 완료 직후 | 결과 후처리, 연쇄 자동화 |
| Stop | 에이전트 루프 종료 직전 | 완료 여부 검증, 데스크톱 알림 |
저도 처음엔 "CLAUDE.md에 규칙 써두면 되지 않나?" 싶었는데, 결국 CLAUDE.md는 Claude가 읽고 따르려고 노력하는 지침이고, 훅은 시스템이 강제하는 규칙입니다. 이 차이가 실무에서 꽤 크게 체감됩니다.
미리 알아두면 좋은 주의사항
실전 예시로 넘어가기 전에, 훅을 처음 쓸 때 반드시 알아두어야 할 점들을 먼저 짚어두겠습니다.
- 샌드박스가 없습니다. 훅은 여러분의 사용자 권한으로 실행됩니다. 잘못 작성하면 파일 삭제나 시크릿 노출이 발생할 수 있으니, 스크립트는 작고 명시적으로 작성하는 것이 좋습니다.
- 훅은 방어의 한 레이어입니다. 하나의 도구를 막아도 모델이 다른 경로로 동일 결과를 달성할 수 있습니다. Git hooks나 CI/CD 검증과 함께 쓰는 것이 안전합니다.
- 신뢰하지 않는 레포는 주의하세요. 프로젝트 레벨 훅(
.claude/settings.json)은 해당 레포를 열면 자동 실행될 수 있습니다. 외부 레포를 클론할 때 훅 내용을 확인하는 습관을 갖는 것이 좋습니다.
3가지 핸들러 타입
훅을 어떻게 구현할지는 세 가지 방식 중에서 고를 수 있습니다.
Command — 셸 커맨드를 직접 실행합니다. stdin으로 JSON을 수신하고, exit code와 stdout JSON으로 결과를 반환합니다. 가장 범용적이고 유연합니다. 실무에서 압도적으로 많이 쓰입니다.
Prompt — Claude 모델에게 단일 턴 평가를 요청합니다. 패턴 매칭으로 잡기 어려운 의미론적 판단이 필요할 때 유용합니다.
Agent — 서브에이전트를 생성합니다. Read·Grep·Glob 등의 도구에 접근 가능해 복잡한 검증 로직에 활용됩니다.
간단한 경로 검사나 파일 스캔은 Python이나 bash로, 외부 도구 연동은 bash로, 의미론적 판단(아키텍처 규칙 위반 여부 등)은 Prompt 타입으로 쓰면 됩니다.
통신 프로토콜 — stdin/stdout JSON
훅 스크립트의 동작 방식을 한 줄로 요약하면 이렇습니다. stdin으로 도구 실행 정보를 JSON으로 받고, stdout으로 제어 신호를 JSON으로 돌려줍니다.
[Claude] → stdin(JSON) → [훅 스크립트] → stdout(JSON) → [Claude]// stdin으로 수신 (PreToolUse - Write 도구 예시)
{
"session_id": "abc123",
"tool_name": "Write",
"tool_input": {
"file_path": "/src/app.ts",
"content": "..."
}
}// stdout으로 반환 (PreToolUse 차단 예시)
{
"hookSpecificOutput": {
"decision": "block",
"reason": ".env 파일은 수동 승인이 필요합니다"
}
}한 가지 중요한 차이가 있는데, PreToolUse와 PostToolUse의 제어 방식이 다릅니다.
- PreToolUse:
decision필드("block"/"approve")로 도구 실행 여부를 제어합니다. stdout에 JSON을 반환하고 exit 0으로 종료합니다. - PostToolUse: 도구는 이미 실행됐으므로 취소할 수 없습니다. 에러 시그널은 비-0 exit code로 전달하고, Claude에게 피드백을 전달할 때는 stdout을 씁니다.
이 두 가지를 혼용하면 예상치 못한 동작이 발생하니 주의하는 것이 좋습니다.
bash 스크립트에서 stdin 읽는 방법도 짚고 넘어가겠습니다. $STDIN은 환경 변수가 아니라 stdin 스트림으로 전달되기 때문에, 반드시 cat으로 읽어야 합니다.
# 올바른 방법
STDIN_DATA=$(cat)
FILE_PATH=$(echo "$STDIN_DATA" | jq -r '.tool_input.file_path // empty')settings.json 기본 설정 구조
훅은 Claude Code의 settings.json에 등록합니다. 전역 설정(~/.claude/settings.json)과 프로젝트 레벨(.claude/settings.json) 모두 지원됩니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/check_write.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/auto_format.sh"
}
]
}
]
}
}matcher는 정규식을 지원합니다. Edit|Write처럼 파이프로 여러 도구를 한 번에 매칭할 수 있어 편리합니다. jq, gitleaks, osascript 같은 외부 도구를 쓰는 예시들이 아래에 나오는데, 사전에 설치 여부를 확인해 두는 것이 좋습니다.
실전 적용
이제 실제로 어떻게 쓰는지 살펴보겠습니다.
예시 1: .env 파일 보호 — PreToolUse로 민감 파일 수정 차단
솔직히 이게 훅을 처음 써본 계기였습니다. Claude가 열심히 코드를 짜다가 .env를 건드리는 걸 보고 식은땀이 났거든요. PreToolUse에 Python 스크립트 하나 걸어두는 것만으로 해결됩니다.
# ~/.claude/hooks/check_sensitive.py
import json
import sys
data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "")
sensitive_patterns = [".env", "secrets", ".pem", ".key", "credentials"]
if any(pattern in path for pattern in sensitive_patterns):
print(json.dumps({
"hookSpecificOutput": {
"decision": "block",
"reason": f"민감 파일({path}) 수정은 수동 승인이 필요합니다"
}
}))
sys.exit(0)// ~/.claude/settings.json (전역 적용)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/check_sensitive.py"
}
]
}
]
}
}| 코드 포인트 | 설명 |
|---|---|
json.load(sys.stdin) |
stdin 스트림에서 도구 실행 정보를 파싱 |
sensitive_patterns 리스트 |
차단할 경로 패턴을 확장 가능하게 관리 |
decision: "block" |
Claude에게 이 도구 실행을 중단하도록 신호 전달 |
reason 필드 |
Claude가 사용자에게 보여줄 차단 이유 메시지 |
예시 2: 자동 포매팅 — PostToolUse로 편집 즉시 Prettier 실행
파일을 수정할 때마다 수동으로 포매팅하는 건 정말 번거로운 일인데, 이 훅을 걸고 나서 팀 코드 리뷰에서 스타일 지적이 눈에 띄게 줄었습니다. PostToolUse를 활용하면 Claude가 파일을 편집하는 순간 자동으로 Prettier가 실행됩니다.
#!/bin/bash
# ~/.claude/hooks/auto_format.sh
# stdin 스트림에서 데이터 읽기
STDIN_DATA=$(cat)
FILE_PATH=$(echo "$STDIN_DATA" | jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# TypeScript/JavaScript/JSON 파일에만 Prettier 적용
if echo "$FILE_PATH" | grep -qE '\.(ts|tsx|js|jsx|json|css|md)$'; then
npx prettier --write "$FILE_PATH" 2>/dev/null
fi
# TypeScript 파일이면 타입 체크도 함께 — 에러가 있으면 stdout으로 Claude에게 전달
if echo "$FILE_PATH" | grep -qE '\.tsx?$'; then
TSC_OUTPUT=$(npx tsc --noEmit 2>&1 | head -20)
if [ -n "$TSC_OUTPUT" ]; then
echo "$TSC_OUTPUT" # Claude가 이 내용을 참고해 다음 행동을 결정합니다
fi
fi
exit 0// .claude/settings.json (프로젝트 레벨 적용)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/auto_format.sh"
}
]
}
]
}
}예시 3: 시크릿 스캔 — PostToolUse로 API 키 유출 방지
코드 작성 중에 실수로 API 키가 파일에 들어가는 경우, 커밋 전에 잡아내는 게 이상적이지만 그보다 더 일찍 — 파일이 저장되는 순간 — 잡아낼 수 있습니다. gitleaks가 설치되어 있다면 바로 쓸 수 있습니다.
#!/bin/bash
# ~/.claude/hooks/secret_scan.sh
STDIN_DATA=$(cat)
FILE_PATH=$(echo "$STDIN_DATA" | jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
exit 0
fi
# gitleaks로 방금 작성된 파일 스캔
if command -v gitleaks &> /dev/null; then
RESULT=$(gitleaks detect --source "$(dirname "$FILE_PATH")" \
--no-git --quiet 2>&1)
if [ $? -ne 0 ]; then
# PostToolUse에서 에러 시그널은 비-0 exit code로 전달
echo "⚠️ 시크릿 감지: $FILE_PATH 에서 잠재적 시크릿이 발견됐습니다"
echo "$RESULT"
exit 1
fi
fi
exit 0예시 4: 아키텍처 규칙 검증 — Prompt 타입으로 의미론적 판단
NestJS를 쓰다 보면 Controller에 비즈니스 로직이 슬며시 들어가는 경우가 있습니다. 이건 단순 패턴 매칭으로는 잡기 어려운데, Prompt 타입 훅을 쓰면 Claude 모델이 직접 판단해줍니다.
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "다음 코드가 NestJS Controller 파일(*.controller.ts)에 작성될 예정입니다. 비즈니스 로직(데이터 변환, 계산, DB 직접 접근)이 Controller에 직접 구현되려는 시도인지 판단하세요. 그렇다면 hookSpecificOutput.decision을 'block'으로, reason에 이유를 담아 JSON으로 반환하세요. 문제없으면 아무것도 반환하지 마세요. 코드: $ARGUMENTS"
}
]
}
]
}
}Prompt 타입 훅 주의사항: 매 도구 실행마다 추가 LLM 호출이 발생하므로 응답 속도가 느려집니다.
matcher를.*controller\.ts$처럼 좁혀서 핵심 파일에만 선별적으로 적용하는 것이 좋습니다.
예시 5: 작업 완료 알림 — Stop 이벤트로 데스크톱 알림
긴 리팩토링 작업을 Claude에게 맡겨놓고 다른 일을 하다 보면 언제 끝났는지 놓치기 쉽습니다. Stop 이벤트 훅 하나로 macOS 알림을 받을 수 있습니다.
#!/bin/bash
# ~/.claude/hooks/notify_done.sh
# macOS
if command -v osascript &> /dev/null; then
osascript -e 'display notification "작업이 완료됐습니다" with title "Claude Code" sound name "Glass"'
fi
# Linux (notify-send)
if command -v notify-send &> /dev/null; then
notify-send "Claude Code" "작업이 완료됐습니다"
fi
exit 0// ~/.claude/settings.json
{
"hooks": {
"Stop": [
{
// Stop 이벤트는 특정 도구 없이 루프 종료 시점에 실행되므로 matcher 불필요
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/notify_done.sh"
}
]
}
]
}
}장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 결정론적 실행 | LLM이 지시를 잊어도 훅은 항상 실행됩니다 |
| 즉각적 피드백 | 편집 즉시 lint·type check 수행으로 오류 조기 발견이 가능합니다 |
| 세밀한 제어 | 도구별, 파일 패턴별 매칭으로 정밀하게 적용할 수 있습니다 |
| 언어 무관 | Python·Bash·Node.js 등 익숙한 언어로 바로 작성 가능합니다 |
| MCP 연동 | MCP 서버 도구도 동일한 방식으로 훅 매칭이 가능합니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 샌드박스 없음 | 훅은 사용자 권한으로 실행됨 — 잘못 작성 시 파일 삭제·시크릿 노출 가능 | 스크립트를 소규모·명시적으로 작성, 코드 리뷰 권장 |
| 우회 가능성 | 특정 도구를 막아도 모델이 다른 경로로 동일 결과 달성 가능 | defense-in-depth 전략의 한 레이어로만 활용 |
| GUI 없음 | 시각적 편집기·샌드박스 테스트 없이 JSON과 코드만으로 관리 | 로컬에서 직접 스크립트 실행해 사전 검증 |
| 서브에이전트 미적용 | 서브에이전트·파이프 모드에는 훅이 적용되지 않음 | 중요 규칙은 Git hooks와 이중 적용 권장 |
| 악성 프로젝트 위험 | 신뢰하지 않는 레포 로드 시 프로젝트 레벨 훅 자동 실행 가능 | 프로젝트 훅 내용 확인 후 실행 |
| 유지보수 부담 | 프로젝트 변화에 따라 훅 스크립트도 지속 관리 필요 | 훅을 프로덕션 코드처럼 버전 관리 |
실제로 가장 고통스러웠던 점은 GUI 없이 JSON만으로 디버깅하는 것이었습니다. 훅이 왜 안 되는지 파악하려면 스크립트를 직접 터미널에서 실행해보는 게 제일 빠릅니다.
⚠️ 흔한 실수와 트러블슈팅
- bash에서
$STDIN환경 변수를 쓰는 것 — 훅은 환경 변수가 아닌 stdin 스트림으로 데이터를 전달합니다.STDIN_DATA=$(cat)으로 먼저 읽어야 합니다. 이 실수를 하면FILE_PATH가 항상 빈 값이 되어 훅이 아무것도 하지 않습니다. - exit code와 JSON stdout을 혼용하는 것 — PreToolUse에서
exit 1로 차단하면서 동시에decision: "block"JSON을 출력하면 예상치 못한 동작이 발생합니다. 방식을 하나로 통일하는 것이 좋습니다. - 무거운 훅을 모든 도구에 거는 것 — Prompt 타입이나 무거운 스캔을
matcher: ".*"으로 전체 도구에 걸면 응답 속도가 크게 저하됩니다. 핵심 도구와 파일에만 선별적으로 적용하는 것이 좋습니다.
마치며
Claude Code Hooks는 "LLM은 잊는다"는 근본적인 한계를 코드로 우회하는 가장 실용적인 방법입니다.
지금 바로 시작해볼 수 있는 3단계:
- 첫 번째 훅을 등록해 보세요.
~/.claude/settings.json에 위의.env보호 예시를 복사해 붙여넣고,~/.claude/hooks/check_sensitive.py파일을 만들어 저장하면 됩니다. 다음 Claude Code 세션부터 즉시 적용됩니다. - PostToolUse 포매팅 훅을 프로젝트에 추가해 보세요.
auto_format.sh를 프로젝트의.claude/settings.json에 등록하면, Claude가 파일을 편집할 때마다 Prettier가 자동으로 실행되는 경험을 바로 확인할 수 있습니다. - 커뮤니티 레시피를 참고해 나만의 파이프라인으로 확장해 보세요.
disler/claude-code-hooks-mastery나hesreallyhim/awesome-claude-code에 이미 검증된 20개 이상의 훅 예제가 공유되어 있으니, 팀의 컨벤션에 맞게 골라 활용해 보시면 좋겠습니다.
다음 글: Claude Code MCP 서버와 Hooks를 통합해 외부 DB·API 호출까지 자동화하는 풀스택 에이전트 파이프라인 구성하기
참고 자료
- Hooks reference — 공식 Claude Code 문서
- Automate workflows with hooks — 공식 가이드
- Claude Code Hooks: A Practical Guide to Workflow Automation — DataCamp
- Automate Your AI Workflows with Claude Code Hooks — GitButler Blog
- Claude Code hooks: A practical guide with examples (2026) — eesel AI
- Claude Code Hooks: Complete Guide with 20+ Ready-to-Use Examples — DEV Community
- Claude Code Hooks Guide 2026: Automate Your AI Coding Workflow — DEV Community
- claude-code-hooks-mastery — GitHub (disler)
- awesome-claude-code — GitHub (hesreallyhim)
- Claude Code Hooks: Guardrails That Actually Work — paddo.dev
- Inside Claude Code: Architecture Behind Tools, Memory, Hooks, and MCP — Penligent
- Claude Code Hooks Master Guide — 18 Events — Claude Lab