Claude Code Hooks — PreToolUse·PostToolUse로 에이전트의 도구 실행을 코드로 제어하기
공식 문서 기반 | Claude Code hooks · PreToolUse · PostToolUse
Claude Code를 도입하고 나서 얼마 지나지 않아, 저도 비슷한 불안감을 느꼈습니다. "Claude가 알아서 rm -rf를 치면 어떡하지?" 팀에 처음 AI 에이전트를 붙여보던 시절, 신뢰와 긴장 사이에서 꽤 오랫동안 줄타기를 했거든요. 그러다 공식 문서에서 Hooks를 발견하고 나서 상황이 달라졌습니다.
이 글을 다 읽고 나면, Claude가 어떤 도구를 실행하기 직전과 직후에 여러분만의 로직을 끼워 넣는 방법을 알게 됩니다. 위험 명령어 차단, 자동 포맷팅, 감사 로그, CI 연동까지 — 이 글에서는 PreToolUse로 도구 실행을 사전에 차단하고, PostToolUse로 반응형 자동화를 구성하는 패턴을 Claude Code 공식 문서를 기반으로 살펴봅니다.
Hooks는 겉으로 보면 단순한 이벤트 리스너처럼 생겼습니다. 그런데 핵심은 타이밍입니다. 실행 전에 끼어들 수 있느냐, 후에만 반응할 수 있느냐 — 이 차이가 "막을 수 있는 것"과 "막을 수 없는 것"을 나눕니다. LLM에게 "이런 건 하지 말아줘"라고 프롬프트로 부탁하는 것과, 훅으로 실행 자체를 차단하는 것은 신뢰도 면에서 완전히 다른 이야기입니다.
핵심 개념
훅이 끼어드는 정확한 타이밍
Claude Code의 에이전트 루프는 이런 흐름으로 흘러갑니다.
SessionStart → PreToolUse → (도구 실행) → PostToolUse → ... → StopPreToolUse는 Claude가 도구를 실행하기 직전에 발동합니다. 아직 아무 일도 일어나지 않은 상태입니다. 이 시점에 훅이 deny를 반환하면 해당 도구 호출 자체가 취소됩니다. 심지어 도구에 넘길 입력값을 수정하는 것도 가능합니다.
PostToolUse는 도구 실행이 완료된 직후에 발동합니다. 솔직히 처음엔 이걸로도 뭔가를 막을 수 있을 거라 기대했는데, 이미 파일이 쓰인 뒤입니다. 롤백은 불가능합니다. 대신 자동 포맷팅, 테스트 실행, 감사 로그 기록처럼 "이미 일어난 일에 반응하는 자동화"에 딱 맞습니다.
에이전트 루프(Agent Loop): Claude Code가 사용자 요청을 처리하기 위해 반복적으로 도구를 선택하고 실행하는 내부 사이클. 훅은 이 루프의 특정 시점에 사용자 정의 로직을 주입합니다.
훅 등록과 기본 설정
설정 파일은 두 곳에 둘 수 있습니다. 전역 설정은 ~/.claude/settings.json, 프로젝트 한정 설정은 .claude/settings.json입니다. 구조는 이렇게 생겼습니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash /path/to/validator.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "prettier --write $TOOL_INPUT_FILE_PATH" }
]
}
]
}
}matcher에는 도구 이름을 정규식으로 지정합니다. 기본적으로 대소문자를 구분하며, 도구 이름은 정확히 맞춰야 합니다(bash가 아니라 Bash). |로 여러 도구를 동시에 잡을 수 있고(Write|Edit), .*로 모든 도구를 대상으로 할 수도 있습니다.
핸들러 타입은 다섯 가지입니다.
| 타입 | 안정성 | 설명 |
|---|---|---|
command |
GA | Shell 명령어 또는 스크립트 실행 |
http |
GA | 외부 HTTP 엔드포인트 호출 |
mcp_tool |
GA | 연결된 MCP 서버의 도구 호출 |
prompt |
GA | Claude 모델을 단일 턴 판정자로 활용 |
agent |
실험적 | Claude Code 서브에이전트 실행 |
훅이 stdin으로 받는 데이터 구조
훅 스크립트는 stdin으로 도구 호출 정보를 JSON 형태로 받습니다. Bash 도구를 예로 들면 이렇게 생겼습니다.
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build",
"description": "빌드 디렉토리 정리"
}
}jq -r '.tool_input.command'로 실제 명령어를 꺼내는 이유가 여기 있습니다. 도구 타입마다 tool_input의 키 구조가 다르기 때문에, 새로운 도구를 대상으로 훅을 작성할 때는 이 스키마를 먼저 확인하는 습관이 중요합니다.
결정은 stdout에 이런 형식으로 반환합니다.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "위험한 명령어입니다"
}
}permissionDecision에는 allow, deny, ask(사용자에게 확인 요청) 중 하나를 넣을 수 있습니다.
exit code — 처음엔 헷갈리는 부분
저도 이 부분에서 한참 삽질했는데, 한 번 정리해두면 나중에 편합니다.
| exit code | 의미 |
|---|---|
0 |
성공. stdout에 JSON이 있으면 파싱해서 사용 |
2 |
블로킹 오류. PreToolUse에서 작업이 중단되고 stderr 내용이 Claude에게 피드백됨 |
| 그 외 비제로 | 비블로킹 오류. 실행은 계속되고 stderr는 verbose 모드에서만 표시 |
블로킹 오류(exit 2): 스크립트가 단순히 실패한 게 아니라 "이 작업을 진행하면 안 된다"는 신호입니다. Claude가 stderr 내용을 읽고 판단을 재조정합니다.
deny 결정 자체는 성공적으로 처리된 것이므로 exit 0으로 끝내는 것이 맞습니다. exit 0이 "도구를 허용한다"는 뜻이 아닙니다 — stdout에 담긴 JSON이 실제 결정을 담고 있습니다.
실전 적용
예시는 간단한 것부터 복잡한 순서로 배치했습니다. 훅을 처음 도입할 때는 예시 1부터 시작해보시면 진입 장벽이 낮습니다.
예시 1: 파일 저장 후 자동 포맷팅 (PostToolUse)
PostToolUse의 자동 포맷팅이 가장 시작하기 좋습니다. Claude가 파일을 쓸 때마다 Prettier가 자동으로 따라붙는 구조인데, PR에서 "왜 포맷팅이 안 되어 있어요?" 소리를 들을 일이 없어집니다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "prettier --write \"$TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
}]
}
]
}
}$TOOL_INPUT_FILE_PATH는 Claude Code가 PostToolUse 훅 실행 시 자동으로 주입해주는 환경변수입니다. 별도로 export할 필요 없이 바로 참조할 수 있습니다. || true는 Prettier가 지원하지 않는 파일 형식(예: .sh, .md)에서 훅 자체가 오류로 처리되지 않도록 하는 방어 코드입니다.
예시 2: 위험 명령어 자동 차단 (PreToolUse)
rm -rf나 강제 push 같은 명령어를 Claude가 실행하려 할 때 사전에 막는 패턴입니다.
이 예시에서는 jq를 사용합니다. 설치되어 있지 않다면 macOS는 brew install jq, Ubuntu/Debian은 apt install jq로 설치할 수 있습니다. 팀 개발 환경에 미리 포함되어 있는지 확인해두는 것이 좋습니다.
#!/bin/bash
# pre-bash-guard.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
BLOCKED_PATTERNS=("rm -rf /" "git push --force" ":(){:|:&};:" "chmod 777")
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qF -- "$pattern"; then
echo '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "위험 명령어가 감지되어 실행을 차단했습니다"
}
}'
exit 0
fi
done{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "bash pre-bash-guard.sh" }]
}
]
}
}| 코드 포인트 | 설명 |
|---|---|
INPUT=$(cat) |
stdin으로 들어오는 도구 입력 JSON 전체를 읽음 |
jq -r '.tool_input.command' |
Bash 도구의 실제 명령어 문자열 추출 |
grep -qF -- "$pattern" |
고정 문자열로 검색(-F). 패턴에 공백이나 특수문자가 있어도 단어 분리 없이 안전하게 동작 |
permissionDecision: "deny" |
Claude에게 "이 도구 호출을 취소해라"라고 전달 |
exit 0 |
deny 결정 자체는 성공적으로 처리됐음을 의미 — 허용이 아님 |
예시 3: 비동기 감사 로그 (PostToolUse, async)
모든 도구 호출을 로깅하고 싶은데 로깅 때문에 에이전트 루프가 느려지는 건 피하고 싶다면 async: true를 활용할 수 있습니다. 저희 팀에서는 에이전트가 실제로 어떤 도구를 얼마나 호출하는지 파악하는 데 이 패턴이 큰 도움이 됐습니다. 정책을 다듬을 때 눈에 보이는 데이터가 있으니까요.
{
"hooks": {
"PostToolUse": [
{
"matcher": ".*",
"hooks": [{
"type": "command",
"command": "bash audit-logger.sh",
"async": true
}]
}
]
}
}async: true를 붙이면 Claude의 루프를 블로킹하지 않고 백그라운드에서 실행됩니다. asyncRewake: true 옵션을 추가하면 로깅이 완료된 후 Claude를 다시 깨워 후속 처리를 이어가게 할 수도 있습니다. 단, async: true는 type: "command"에서만 지원됩니다. prompt나 agent 타입 훅에는 적용되지 않습니다.
예시 4: HTTP 훅으로 CI 트리거
외부 CI 시스템과 연동이 필요한 경우 type: "http" 핸들러를 활용할 수 있습니다. 처음 이 기능을 봤을 때 "webhook을 이렇게 간단하게 붙일 수 있구나"라는 느낌이었는데, 실제로도 설정 자체는 단순합니다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "http",
"url": "https://ci.example.com/webhook",
"headers": { "Authorization": "Bearer ${CI_TOKEN}" },
"allowedEnvVars": ["CI_TOKEN"]
}]
}
]
}
}headers 안의 ${CI_TOKEN}은 shell 보간이 아닙니다. allowedEnvVars에 명시된 환경변수 이름을 JSON 설정 안에서 ${VARNAME} 형식으로 참조하는 Claude Code 자체 문법입니다. allowedEnvVars에 이름이 없으면 보안상의 이유로 값이 주입되지 않습니다.
예시 5: 프로덕션 경로 보호 (PreToolUse + Prompt 훅)
type: "prompt" 핸들러는 Claude 모델 자체를 단일 턴 판정자로 쓰는 방식입니다. 복잡한 조건을 shell 스크립트로 코딩하지 않고 자연어로 정의할 수 있어서 유연합니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [{
"type": "prompt",
"prompt": "이 파일 경로가 /prod/ 또는 /release/ 디렉토리를 포함하면 'deny', 그렇지 않으면 'allow'로만 응답하세요."
}]
}
]
}
}다만 이 방식에는 실용적인 한계가 있습니다. LLM이 deny 또는 allow 한 단어만 반환해야 하는데, 간혹 "deny입니다" 또는 "allow가 맞습니다"처럼 자연어로 응답하면 파싱이 실패할 수 있습니다. 명확한 경로 패턴은 shell 스크립트로 처리하고, prompt 훅은 규칙으로 표현하기 어려운 의미론적 판단에 보조 수단으로 쓰는 것이 더 안정적입니다. LLM 호출이 발생하므로 레이턴시와 비용도 감안하는 것이 좋습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 결정론적 제어 | LLM 판단에 의존하지 않고 규칙 기반으로 도구 실행을 보장할 수 있음 |
| 보안 게이트 | PreToolUse로 위험 작업을 에이전트 루프 외부에서 원천 차단 가능 |
| 자동화 일관성 | 포맷팅, 테스트, 알림 등을 모든 세션에 균일하게 적용 |
| 핸들러 조합 | command · http · mcp_tool · prompt · agent를 혼합해서 활용 가능 |
| 범위 분리 | 전역/프로젝트 레벨 설정을 독립적으로 관리 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| PostToolUse 롤백 불가 | 이미 파일이 쓰인 후 트리거되므로 변경사항을 되돌릴 수 없음 | 위험한 파일 쓰기는 반드시 PreToolUse로 제어 |
| stdin 파싱 의존 | 훅 스크립트가 JSON stdin을 직접 파싱해야 해서 jq 등 도구 설치 필요 |
팀 개발 환경에 jq 포함 여부를 사전 확인 |
| 동기 훅 성능 영향 | 느린 동기 훅이 모든 도구 호출을 블로킹해 UX가 나빠질 수 있음 | 오래 걸리는 작업은 async: true 적용 |
| async 타입 제약 | 비동기 실행은 type: "command"만 지원, prompt·agent 훅은 불가 |
prompt 훅은 경량 판정 로직에만 사용 |
| prompt 훅 파싱 불안정 | LLM이 deny/allow 외 형식으로 응답할 경우 파싱 실패 가능 |
명확한 조건은 shell 스크립트로 처리, prompt 훅은 보조 수단으로 활용 |
| 디버깅 복잡성 | 여러 훅이 중첩될 때 실행 순서와 exit code 추적이 어려움 | verbose 모드(--verbose)로 stderr 출력을 확인 |
실무에서 가장 흔한 실수
-
PostToolUse로 파일 삭제나 DB 변경을 막으려는 시도 — 이미 실행된 뒤입니다. 막고 싶다면 반드시 PreToolUse를 써야 합니다.
-
exit code 0과 2를 혼동하는 경우 — "스크립트가 정상 종료됐으니 도구도 허용되겠지"라고 생각하기 쉬운데,
deny결정은 JSON stdout으로 전달하는 거지 exit code로 결정되지 않습니다.exit 2는 "뭔가 잘못됐으니 멈춰라"는 별도의 신호입니다. -
matcher에 도구 이름을 소문자로 쓰는 경우 —
bash라고 쓰면 매칭이 안 됩니다. 도구 이름은 대소문자를 정확히 맞춰Bash,Write,Edit으로 작성하는 것을 권장합니다.
마치며
Claude Code Hooks는 AI 에이전트에 "우리 팀의 규칙"을 코드로 심는 가장 확실한 방법입니다. 그리고 이 셋을 조합하기 시작하면 단순한 차단 규칙을 넘어서는 그림이 그려집니다. 포맷팅 훅으로 코드 품질을 자동 관리하고, 보안 훅으로 위험 명령어를 막고, 감사 로그로 에이전트의 행동 패턴을 이해하다 보면 — Claude가 점점 팀의 작업 방식에 맞게 길들여지는 느낌이 납니다.
지금 시작해볼 수 있는 3단계:
-
~/.claude/settings.json에 Prettier 자동 포맷팅 설정 한 줄만 추가해볼 수 있습니다. Claude가 파일을 쓸 때마다 포맷팅이 따라붙는 걸 바로 확인할 수 있어서, 훅이 실제로 어떻게 동작하는지 감을 잡기에 가장 좋은 시작점입니다. -
PreToolUse로 위험 명령어 차단 스크립트를 팀 저장소의
.claude/settings.json에 추가해볼 수 있습니다. 처음엔deny대신ask로 설정해두면, Claude가 사용자에게 한 번 더 확인을 요청하는 방식으로 부드럽게 운영할 수 있습니다. -
async: true로 감사 로그를 붙여볼 수 있습니다.matcher: ".*"로 모든 도구를 잡아 로컬 파일에 기록해두면, 에이전트가 실제로 어떤 도구를 얼마나 호출하는지 눈으로 확인하면서 정책을 다듬어 나갈 수 있습니다.
훅을 하나씩 붙여가다 보면, Claude를 더 믿고 쓸 수 있게 됩니다. 신뢰는 막연한 낙관이 아니라, 이런 구조에서 나오는 거니까요.
참고 자료
공식 문서
커뮤니티 가이드
- Claude Code Hooks: Complete Guide to All 12 Lifecycle Events — claudefa.st (커뮤니티)
- Claude Code Hooks: 6 Production Patterns (2026) — Pixelmojo (커뮤니티)
- Claude Code Hooks: A Practical Guide to Workflow Automation — DataCamp
- Claude Code Hooks Complete Guide (March 2026 Edition) — SmartScope (커뮤니티)
- Implementing PostToolUse Hooks — CodeSignal
- Claude Code Hooks: Security Gates for Agent Workflows — DEV Community
- Claude Code Hook Control Flow — Steve Kinney
- Claude Code Hooks: Complete Reference (32+ Events) — The Prompt Shelf (커뮤니티)
- GitHub - claude-code-hooks-mastery
- Understanding Claude Code Hooks Documentation — PromptLayer Blog