Claude Code에는 Hooks라는 기능이 있다. 도구 실행 전후에 셸 명령을 자동으로 끼워 넣는 기능이다.
Git hooks를 써본 적 있으면 감이 올 것이다. 커밋 전에 린트를 돌리고, 푸시 전에 테스트를 돌리는 그 구조다. Hooks는 그걸 AI 에이전트 레벨에서 하는 것이다. Claude가 파일을 수정하기 전에 백업을 뜨고, 위험한 명령을 실행하려 하면 차단하고, 작업이 끝나면 Slack으로 알림을 보낸다.
설정 한 번으로 반복적인 안전장치와 자동화가 걸린다. 매번 프롬프트에 "이건 하지 마"라고 쓸 필요가 없다.
목차
- Hooks가 뭔가
- PreToolUse
- PostToolUse
- Notification
- 기본 구조
- 입출력 프로토콜
- PreToolUse — 입력 (stdin)
- PreToolUse — 출력 (stdout)
- PostToolUse — 입력 (stdin)
- Notification — 입력 (stdin)
- 실전 레시피 8가지
- 레시피 1: 위험한 명령어 차단
- 레시피 2: 테스트 출력 필터링 (토큰 절약)
- 레시피 3: 파일 수정 시 자동 백업
- 레시피 4: 커밋 메시지 자동 검증
- 레시피 5: Slack/Discord 알림
- 레시피 6: 특정 디렉토리 수정 금지
- 레시피 7: 빌드 명령어 자동 타임아웃
- 레시피 8: 작업 로그 자동 기록
- 레시피 조합 — 실전 settings.json
- 디버깅 팁
- 주의사항
- 보안
- 무한 루프
- 설정 파일 위치
- 요약
Hooks가 뭔가
Hooks는 Claude Code 이벤트에 반응해서 실행되는 셸 명령이다. settings.json에 설정하면, 특정 도구가 실행될 때 자동으로 지정한 스크립트가 돌아간다.
핵심은 세 가지 이벤트 타입이다:
PreToolUse
도구가 실행되기 전에 동작한다. 가장 강력한 훅이다.
- 명령어를 수정할 수 있다 (테스트 출력에
| head -50붙이기) - 실행을 차단할 수 있다 (
rm -rf /같은 위험 명령 거부) - 입력을 변환할 수 있다 (타임아웃 래핑 등)
- 아무것도 안 하면 그냥 통과시킨다
PostToolUse
도구가 실행된 후에 동작한다.
- 결과를 로깅할 수 있다 (모든 도구 호출을 파일에 기록)
- 외부 알림을 보낼 수 있다 (Slack, Discord)
- 출력을 수정할 수 있다 (민감 정보 마스킹)
Notification
Claude가 알림을 보낼 때 동작한다. 주로 작업 완료 시점이다.
- 장시간 작업 후 데스크톱 알림
- 메신저 알림
- 사운드 재생
세 타입 모두 공통점이 있다: stdin으로 JSON을 받고, stdout으로 JSON을 내보낸다. 출력이 없으면 기본 동작(허용)으로 처리된다.
기본 구조
프로젝트 루트의 .claude/settings.json에 설정한다:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/block-dangerous.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/log-all.sh"
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/slack-notify.sh"
}
]
}
]
}
}
각 필드의 의미:
matcher: 어떤 도구에 훅을 걸 것인가."Bash","Edit","Write","Read"등 도구명을 지정한다."*"이면 모든 도구에 적용된다. Notification은 도구가 아니므로 빈 문자열이다.type: 항상"command"이다. 현재는 셸 명령만 지원한다.command: 실행할 셸 명령 또는 스크립트 경로다. 절대경로나~경로를 쓴다.
설정 파일은 두 곳에 둘 수 있다:
- 프로젝트별:
.claude/settings.json— 해당 프로젝트에서만 적용 - 글로벌:
~/.claude/settings.json— 모든 프로젝트에 적용
입출력 프로토콜
Hooks는 stdin/stdout으로 JSON을 주고받는다. 이 프로토콜을 정확히 알아야 훅을 작성할 수 있다.
PreToolUse — 입력 (stdin)
{
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
도구 이름과 도구에 전달될 입력이 들어온다. Bash면 command, Edit면 file_path와 old_string/new_string 등이다.
PreToolUse — 출력 (stdout)
세 가지 선택지가 있다.
1. 허용 (기본값)
{}
빈 JSON이거나 아무 출력도 없으면 그대로 실행을 허용한다.
2. 입력을 수정해서 허용
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "npm test 2>&1 | head -50"
}
}
}
원래 명령을 바꿔치기한다. Claude는 수정된 명령이 실행된 줄 모른다.
3. 차단
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"reason": "Production database access blocked"
}
}
실행을 거부한다. reason이 Claude에게 전달되어 왜 차단됐는지 알려준다.
PostToolUse — 입력 (stdin)
{
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
},
"tool_output": "... test results ..."
}
PreToolUse와 동일한 구조에 tool_output이 추가된다. 실행 결과를 볼 수 있다.
Notification — 입력 (stdin)
{
"message": "Task completed successfully"
}
알림 메시지 텍스트가 들어온다.
실전 레시피 8가지
이론은 여기까지다. 바로 쓸 수 있는 레시피 8개를 정리했다.
레시피 1: 위험한 명령어 차단
프로덕션 데이터를 날리거나, 루트 디렉토리를 삭제하거나, 메인 브랜치에 force push하는 걸 막는다. Claude가 아무리 똑똑해도 실수는 한다.
#!/bin/bash
# block-dangerous.sh — PreToolUse, matcher: "Bash"
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
dangerous_patterns=(
"rm -rf /"
"DROP TABLE"
"DROP DATABASE"
"git push.*--force.*main"
"git push.*--force.*master"
"git reset --hard"
"chmod 777"
"mkfs\\."
"> /dev/sda"
)
for pattern in "${dangerous_patterns[@]}"; do
if echo "$cmd" | grep -qiE "$pattern"; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"reason\":\"Blocked: matches dangerous pattern '$pattern'\"}}"
exit 0
fi
done
echo "{}"
패턴 리스트는 프로젝트에 맞게 확장하면 된다. 예를 들어 프로덕션 DB 호스트명을 패턴에 넣으면 실수로 프로덕션에 쿼리 날리는 것도 막을 수 있다.
설정:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/block-dangerous.sh"
}
]
}
]
}
}
레시피 2: 테스트 출력 필터링 (토큰 절약)
npm test를 돌리면 수천 줄의 출력이 나온다. 통과한 테스트 목록은 Claude에게 필요 없다. 실패한 부분만 보여주면 컨텍스트 토큰을 대폭 줄일 수 있다.
#!/bin/bash
# filter-test-output.sh — PreToolUse, matcher: "Bash"
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
if [[ "$cmd" =~ ^(npm\ test|npx\ jest|pytest|go\ test|\.\/gradlew\ test|cargo\ test) ]]; then
filtered="$cmd 2>&1 | grep -A 5 -E '(FAIL|ERROR|error:|FAILED|panic:|assertion)' | head -100"
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered\"}}}"
else
echo "{}"
fi
원래 명령에 grep과 head를 파이프로 붙여서, 에러 관련 줄만 추출한다. 10,000줄 출력이 100줄 이하로 줄어든다. 비용 절약 글에서도 다뤘던 전략인데, 이게 그 구체적인 구현이다.
레시피 3: 파일 수정 시 자동 백업
Claude가 파일을 수정하기 전에 원본을 백업해둔다. Edit이나 Write가 실행될 때마다 동작한다.
#!/bin/bash
# auto-backup.sh — PreToolUse, matcher: "Edit" 또는 "Write"
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
if [ -n "$file" ] && [ -f "$file" ]; then
backup_dir="$HOME/.claude/backups/$(date +%Y%m%d)"
mkdir -p "$backup_dir"
relative_path=$(echo "$file" | sed "s|$HOME/||" | tr '/' '_')
cp "$file" "$backup_dir/${relative_path}.$(date +%H%M%S).bak"
fi
echo "{}"
~/.claude/backups/20260411/ 같은 디렉토리에 날짜별로 쌓인다. 파일 경로를 언더스코어로 변환해서 백업 파일명에 넣으므로 어떤 파일의 백업인지 추적할 수 있다.
설정 — Edit과 Write 두 도구에 동시 적용:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/auto-backup.sh" }
]
},
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/auto-backup.sh" }
]
}
]
}
}
레시피 4: 커밋 메시지 자동 검증
Claude가 git commit을 실행한 후, 커밋 메시지가 Conventional Commit 형식인지 검증한다. 아니면 경고를 남긴다.
#!/bin/bash
# validate-commit.sh — PostToolUse, matcher: "Bash"
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
# git commit 명령인지 확인
if ! echo "$cmd" | grep -qE "^git commit"; then
echo "{}"
exit 0
fi
# 마지막 커밋 메시지 가져오기
last_msg=$(git log -1 --pretty=%s 2>/dev/null)
# Conventional Commit 패턴: type(scope): message
if ! echo "$last_msg" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .+"; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"notification\":\"Warning: commit message '$last_msg' does not follow Conventional Commit format\"}}"
else
echo "{}"
fi
PostToolUse이므로 커밋 자체는 이미 완료된 상태다. 차단이 아니라 경고를 남기는 방식이다. 커밋을 되돌리고 싶으면 PreToolUse에서 커밋 메시지를 미리 파싱해야 하는데, 그건 좀 더 복잡해진다.
레시피 5: Slack/Discord 알림
Claude가 장시간 작업을 끝내면 Slack으로 알림을 보낸다. Notification 이벤트를 사용한다.
#!/bin/bash
# slack-notify.sh — Notification
input=$(cat)
message=$(echo "$input" | jq -r '.message // "Task completed"')
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Slack Webhook
if [ -n "$SLACK_WEBHOOK_URL" ]; then
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\": \"[${timestamp}] Claude Code: ${message}\"}" \
> /dev/null 2>&1
fi
# Discord Webhook (선택)
if [ -n "$DISCORD_WEBHOOK_URL" ]; then
curl -s -X POST "$DISCORD_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"content\": \"[${timestamp}] Claude Code: ${message}\"}" \
> /dev/null 2>&1
fi
echo "{}"
환경변수 SLACK_WEBHOOK_URL이나 DISCORD_WEBHOOK_URL을 설정해두면 된다. 둘 다 없으면 아무것도 안 하고 넘어간다.
설정:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/slack-notify.sh" }
]
}
]
}
}
레시피 6: 특정 디렉토리 수정 금지
generated/, dist/, node_modules/, .git/ 같은 디렉토리는 Claude가 건드리면 안 된다. 자동 생성 파일을 수동 수정하면 다음 빌드에서 덮어씌워지기 때문이다.
#!/bin/bash
# protect-dirs.sh — PreToolUse, matcher: "Edit" 또는 "Write"
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
protected_dirs=(
"/generated/"
"/dist/"
"/build/"
"/node_modules/"
"/.git/"
"/__pycache__/"
"/vendor/"
)
for dir in "${protected_dirs[@]}"; do
if echo "$file" | grep -q "$dir"; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"reason\":\"Protected directory: files under '$dir' should not be manually edited\"}}"
exit 0
fi
done
echo "{}"
Claude에게 "이 디렉토리는 수정하지 마"라고 프롬프트에 쓰는 것보다 훨씬 확실하다. 프롬프트는 무시될 수 있지만, Hooks는 물리적으로 차단한다.
레시피 7: 빌드 명령어 자동 타임아웃
npm run build나 docker build 같은 명령이 10분 이상 돌아가면 문제가 있는 것이다. 자동으로 타임아웃을 건다.
#!/bin/bash
# auto-timeout.sh — PreToolUse, matcher: "Bash"
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
# 타임아웃을 걸 명령 패턴
long_running_patterns=(
"npm run build"
"yarn build"
"docker build"
"gradle build"
"mvn package"
"cargo build"
"make"
)
TIMEOUT_SEC=300 # 5분
for pattern in "${long_running_patterns[@]}"; do
if echo "$cmd" | grep -qE "^${pattern}"; then
wrapped="timeout ${TIMEOUT_SEC} $cmd"
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$wrapped\"}}}"
exit 0
fi
done
echo "{}"
timeout 명령으로 래핑한다. 5분(300초) 안에 안 끝나면 프로세스를 종료한다. Claude Code가 무한 빌드 루프에 빠지는 걸 방지한다.
레시피 8: 작업 로그 자동 기록
모든 도구 호출을 로컬 파일에 기록한다. 나중에 Claude가 뭘 했는지 감사(audit)할 때 유용하다.
#!/bin/bash
# log-all.sh — PostToolUse, matcher: "*"
input=$(cat)
tool=$(echo "$input" | jq -r '.tool_name // "unknown"')
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
log_dir="$HOME/.claude/logs"
mkdir -p "$log_dir"
log_file="$log_dir/$(date +%Y%m%d).jsonl"
# 도구명, 타임스탬프, 입력을 JSONL로 기록
echo "$input" | jq -c "{timestamp: \"$timestamp\", tool: .tool_name, input: .tool_input}" >> "$log_file"
echo "{}"
날짜별 JSONL 파일로 쌓인다. 나중에 jq로 분석할 수 있다:
# 오늘 Claude가 수정한 파일 목록
cat ~/.claude/logs/20260411.jsonl | jq -r 'select(.tool == "Edit") | .input.file_path' | sort -u
# 오늘 실행한 Bash 명령 목록
cat ~/.claude/logs/20260411.jsonl | jq -r 'select(.tool == "Bash") | .input.command'
설정:
{
"hooks": {
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/log-all.sh" }
]
}
]
}
}
레시피 조합 — 실전 settings.json
위 레시피들을 조합한 실전 설정 예시다:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/block-dangerous.sh" },
{ "type": "command", "command": "~/.claude/hooks/filter-test-output.sh" },
{ "type": "command", "command": "~/.claude/hooks/auto-timeout.sh" }
]
},
{
"matcher": "Edit",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/protect-dirs.sh" },
{ "type": "command", "command": "~/.claude/hooks/auto-backup.sh" }
]
},
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/protect-dirs.sh" },
{ "type": "command", "command": "~/.claude/hooks/auto-backup.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/validate-commit.sh" }
]
},
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/log-all.sh" }
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/slack-notify.sh" }
]
}
]
}
}
같은 matcher에 여러 훅을 걸 수 있다. 순서대로 실행되며, 하나라도 deny를 반환하면 이후 훅은 실행되지 않고 도구 호출이 차단된다.
디버깅 팁
Hooks가 기대대로 동작하지 않을 때 확인할 것들:
1. 입력 확인 — 로그를 찍어라
#!/bin/bash
input=$(cat)
echo "$input" >> /tmp/hook-debug.log
# ... 나머지 로직
/tmp/hook-debug.log를 열어서 stdin으로 실제로 뭐가 들어오는지 확인한다. 예상과 다른 JSON 구조가 올 수 있다.
2. stderr 확인
Hook 스크립트의 stderr는 Claude Code에 표시된다. 에러가 나면 터미널에서 바로 볼 수 있다.
3. jq 의존성
거의 모든 훅이 jq를 사용한다. 설치 안 되어 있으면 당연히 동작 안 한다.
# 확인
which jq
# 설치
# macOS
brew install jq
# Ubuntu/Debian
sudo apt install jq
# Windows (Git Bash)
# jq 바이너리를 PATH에 넣기
4. 실행 권한
chmod +x ~/.claude/hooks/*.sh
스크립트에 실행 권한이 없으면 아무 일도 안 일어난다.
5. 성능
Hook은 도구 호출마다 실행된다. 무거운 스크립트는 Claude Code 전체를 느리게 만든다. HTTP 요청(Slack 알림 등)은 백그라운드로 보내고, 파일 I/O는 최소화하라.
주의사항
보안
Hooks는 로컬 머신에서 셸 명령으로 실행된다. .claude/settings.json을 Git에 커밋하면 팀원들의 머신에서도 실행된다. 악의적인 훅이 섞이면 보안 사고가 난다.
- 훅 스크립트의 출처를 확인하라
- 팀 공유 훅은 코드 리뷰를 거쳐라
- 민감한 훅(DB 접근 등)은 글로벌 설정(
~/.claude/settings.json)에 두고 Git에 커밋하지 마라
무한 루프
PreToolUse에서 Bash 훅이 Bash 명령을 호출하면 무한 루프가 발생할 수 있다. 예를 들어 Bash matcher에 걸린 훅이 내부적으로 curl을 실행하고, 그 curl이 다시 Bash 도구로 처리되면서 같은 훅이 또 트리거되는 식이다.
해결법:
- matcher를 최대한 구체적으로 설정한다 (
"*"대신"Bash","Edit"등) - 훅 스크립트 안에서 셸 명령을 직접 실행할 때는 Claude Code의 도구가 아닌 순수 셸에서 돌아가므로 재귀는 보통 발생하지 않지만, 구조가 복잡해지면 주의가 필요하다
설정 파일 위치
| 위치 | 적용 범위 | Git 추적 |
|---|---|---|
.claude/settings.json |
해당 프로젝트만 | 가능 (팀 공유) |
~/.claude/settings.json |
모든 프로젝트 | 불가 (개인용) |
두 곳에 같은 이벤트/matcher 조합이 있으면 둘 다 실행된다. 프로젝트 설정이 글로벌 설정을 덮어쓰는 게 아니라 합쳐진다.
요약
- Hooks는 Claude Code 도구 실행 전후에 셸 명령을 자동 실행하는 기능이다
- PreToolUse: 실행 전 — 차단, 수정, 허용을 제어한다
- PostToolUse: 실행 후 — 로깅, 알림, 검증을 한다
- Notification: 작업 완료 시 — 외부 알림을 보낸다
- stdin으로 JSON을 받고, stdout으로 JSON을 내보내는 단순한 프로토콜이다
- 위험 명령 차단, 테스트 출력 필터링, 자동 백업, 커밋 검증, Slack 알림, 디렉토리 보호, 타임아웃, 감사 로그 — 이 8가지 레시피를 조합하면 대부분의 자동화 요구를 커버한다
- 디버깅은
/tmp/hook-debug.log로 입력을 찍어보는 것부터 시작하라 - 보안과 무한 루프에 주의하라
프롬프트에 "이건 하지 마", "저건 조심해"를 반복해서 쓰고 있다면, 그건 Hooks로 해결할 수 있다는 신호다.
관련글:
- Claude Code 토큰 비용 실전 절약법 — Hooks로 토큰 비용 줄이는 전략
- Claude Code 해부학 Part 10: 권한 시스템 분석 — Hooks가 권한 시스템과 연동되는 구조
'AI 코딩 에이전트' 카테고리의 다른 글
| Claude Code로 하루 만에 사이드 프로젝트 완성하기: 0→배포 전 과정 타임라인 (0) | 2026.04.11 |
|---|---|
| Claude Code 숨겨진 키보드 단축키 & 치트시트: 속도가 2배 되는 조합들 (0) | 2026.04.11 |
| Claude Code CLAUDE.md 작성법 완전 가이드: 프로젝트 성패를 가르는 파일 하나 (0) | 2026.04.11 |
| Claude Code 토큰 비용 실전 절약법: 공식 문서 기반 7가지 전략 (0) | 2026.04.10 |
| GitHub Agent HQ: Copilot 안에서 Claude와 Codex를 동시에 돌리는 구조 (0) | 2026.04.10 |
