Claude Code Hooks 가이드: Pre/Post 실행 훅으로 커스텀 워크플로우 자동화하기
Claude Code Hooks란 무엇인가
Claude Code hooks는 Claude Code가 특정 동작을 수행할 때 자동으로 셸 명령을 실행하는 메커니즘입니다. 파일을 수정하기 전에 린터를 돌리거나, 코드 변경 후 자동으로 테스트를 실행하거나, 작업 완료 시 Slack 알림을 보내는 등의 워크플로우를 설정 파일 하나로 구성할 수 있습니다.
기존에는 이러한 자동화를 위해 별도의 CI/CD 파이프라인이나 에디터 플러그인을 조합해야 했습니다. hooks를 활용하면 Claude Code의 동작 흐름 안에서 직접 자동화 파이프라인을 구축할 수 있어, 개발 루프를 벗어나지 않고도 품질 관리와 알림 체계를 유지할 수 있습니다.
이 가이드에서는 hooks의 내부 아키텍처부터 실전 워크플로우 설정, 고급 패턴, 트러블슈팅까지 전 과정을 다룹니다. Claude Code를 팀 프로젝트에서 활용하는 개발자, 반복 작업을 자동화하려는 개인 개발자 모두를 대상으로 합니다.
Hook 아키텍처
실행 흐름 개요
Claude Code hooks는 이벤트 기반 아키텍처를 따릅니다. Claude Code가 도구(tool)를 호출하거나 알림을 발생시킬 때, 등록된 hook 설정과 매칭 여부를 확인하고, 조건이 맞으면 지정된 셸 명령을 실행합니다.
전체 흐름은 다음과 같습니다:
- Claude Code가 도구 호출을 시도합니다 (예: 파일 쓰기, 명령 실행).
- 등록된 hook 중 해당 이벤트와 매처(matcher)가 일치하는 것을 찾습니다.
- 매칭된 hook의 셸 명령을 실행합니다.
- 명령의 종료 코드와 출력을 확인합니다.
- 결과에 따라 원래 동작을 계속하거나 차단합니다.
Hook 유형
Claude Code는 세 가지 hook 유형을 지원합니다.
PreToolUse — 도구가 실행되기 전에 트리거됩니다. 이 hook에서 종료 코드를 0이 아닌 값으로 반환하면 해당 도구 호출을 차단할 수 있습니다. 파일 변경 전 린팅, 특정 파일에 대한 쓰기 방지, 위험한 명령 실행 차단 등에 활용합니다.
PostToolUse — 도구가 성공적으로 실행된 후에 트리거됩니다. 파일 변경 후 자동 포맷팅, 테스트 실행, 빌드 검증 등 후처리 작업에 적합합니다. PostToolUse hook의 실패는 이미 완료된 동작을 되돌리지는 않지만, Claude에게 오류 메시지를 전달합니다.
Notification — Claude Code가 사용자에게 알림을 보낼 때 트리거됩니다. 장시간 작업의 완료 알림, 에러 발생 시 외부 서비스 연동 등에 활용할 수 있습니다.
Hook 설정 구조
hook은 settings.json 파일에 정의하며, 다음 속성으로 구성됩니다:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"command": "eslint --fix $CLAUDE_FILE_PATH",
"timeout": 30000
}
]
}
}
각 속성의 의미는 다음과 같습니다:
- matcher — hook을 트리거할 도구 이름 패턴입니다. 정확한 도구 이름(예:
Write,Bash)을 지정하거나, 와일드카드 패턴을 사용할 수 있습니다. - command — 실행할 셸 명령입니다. 환경 변수를 통해 컨텍스트 정보를 전달받습니다.
- timeout — 명령 실행의 최대 시간(밀리초)입니다. 기본값은 60초이며, 이 시간을 초과하면 hook이 실패한 것으로 처리됩니다.
설정 파일 위치
hook은 두 가지 레벨에서 설정할 수 있습니다.
프로젝트 레벨 — 프로젝트 루트의 .claude/settings.json에 정의합니다. 해당 프로젝트에서만 적용되며, 버전 관리에 포함시켜 팀 전체가 동일한 hook을 사용하도록 할 수 있습니다.
사용자 레벨 — ~/.claude/settings.json에 정의합니다. 사용자의 모든 프로젝트에 적용됩니다. 개인적인 알림 설정이나 전역 린팅 규칙 등에 적합합니다.
두 레벨의 hook이 모두 존재하면 양쪽 모두 실행됩니다. 프로젝트 레벨 hook이 먼저 실행된 후 사용자 레벨 hook이 실행됩니다.
환경 변수
hook 명령은 다음 환경 변수를 통해 컨텍스트 정보를 전달받습니다:
| 환경 변수 | 설명 |
|---|---|
$CLAUDE_FILE_PATH | 대상 파일 경로 (Write, Read 도구에서 제공) |
$CLAUDE_TOOL_NAME | 트리거된 도구 이름 |
$CLAUDE_TOOL_INPUT | 도구에 전달된 입력(JSON 형식) |
$CLAUDE_PROJECT_DIR | 현재 프로젝트 디렉터리 경로 |
$CLAUDE_SESSION_ID | 현재 세션 식별자 |
이 환경 변수들을 활용하면 hook 명령에서 파일 경로, 도구 입력값 등을 동적으로 참조할 수 있습니다.
일반 워크플로우 설정
자동 코드 포맷팅
파일이 변경될 때마다 자동으로 코드 포맷터를 실행하는 것은 가장 기본적인 hook 활용 사례입니다. PostToolUse hook으로 Write 도구 실행 후 포맷터를 트리거합니다.
Prettier를 사용한 JavaScript/TypeScript 자동 포맷팅:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"command": "npx prettier --write $CLAUDE_FILE_PATH"
}
]
}
}
Black을 사용한 Python 자동 포맷팅:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"command": "python -m black $CLAUDE_FILE_PATH 2>/dev/null || true"
}
]
}
}
포맷터가 설치되어 있지 않은 환경에서 hook이 실패하지 않도록 || true를 명령 끝에 추가하는 것이 좋습니다. 이렇게 하면 포맷터 오류가 Claude의 작업 흐름을 차단하지 않습니다.
타입 체크 자동 실행
TypeScript 프로젝트에서 파일 변경 후 자동으로 타입 체크를 실행하면 타입 오류를 즉시 발견할 수 있습니다:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"command": "npx tsc --noEmit --pretty 2>&1 | head -20"
}
]
}
}
타입 체크 출력이 길어질 수 있으므로 head -20으로 처음 20줄만 표시하도록 제한합니다. Claude는 이 출력을 읽고 타입 오류가 있으면 자동으로 수정을 시도합니다.
자동 테스트 실행
변경된 파일과 관련된 테스트만 선택적으로 실행하면 빠른 피드백 루프를 유지할 수 있습니다:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"command": "npx jest --findRelatedTests $CLAUDE_FILE_PATH --passWithNoTests 2>&1 | tail -5"
}
]
}
}
Jest의 --findRelatedTests 옵션은 변경된 파일에 의존하는 테스트 파일만 자동으로 찾아 실행합니다. --passWithNoTests 옵션은 관련 테스트가 없을 때 hook이 실패하지 않도록 합니다.
Python pytest를 사용하는 경우:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"command": "python -m pytest --tb=short -q 2>&1 | tail -10"
}
]
}
}
파일 보호 (Write Guard)
특정 파일이나 디렉터리에 대한 수정을 차단하는 PreToolUse hook은 프로덕션 설정 파일, 락 파일, 자동 생성 파일 등을 보호하는 데 유용합니다:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"command": "echo $CLAUDE_FILE_PATH | grep -qE '(package-lock\\.json|yarn\\.lock|\\.env)' && echo 'BLOCKED: This file should not be modified directly.' && exit 1 || exit 0"
}
]
}
}
이 hook은 package-lock.json, yarn.lock, .env 파일에 대한 쓰기 시도를 차단합니다. exit 1을 반환하면 Claude Code가 해당 도구 호출을 중단하고, 차단 메시지를 Claude에게 전달합니다.
더 세밀한 제어가 필요하다면 셸 스크립트로 분리할 수 있습니다:
#!/bin/bash
# scripts/write-guard.sh
PROTECTED_PATTERNS=(
"package-lock.json"
"yarn.lock"
".env"
".env.local"
"dist/"
"build/"
"generated/"
)
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if echo "$CLAUDE_FILE_PATH" | grep -q "$pattern"; then
echo "BLOCKED: '$CLAUDE_FILE_PATH' is a protected file matching pattern '$pattern'."
exit 1
fi
done
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"command": "bash scripts/write-guard.sh"
}
]
}
}
Slack 알림 설정
장시간 실행되는 작업이 완료되었을 때 Slack 알림을 보내면 다른 작업을 하면서도 Claude Code의 진행 상황을 추적할 수 있습니다:
{
"hooks": {
"Notification": [
{
"matcher": "",
"command": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"text\": \"Claude Code 알림: '\"$CLAUDE_NOTIFICATION\"'\"}' https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
}
]
}
}
Slack Webhook URL은 환경 변수로 관리하는 것이 보안상 좋습니다:
{
"hooks": {
"Notification": [
{
"matcher": "",
"command": "bash scripts/slack-notify.sh"
}
]
}
}
#!/bin/bash
# scripts/slack-notify.sh
if [ -z "$SLACK_WEBHOOK_URL" ]; then
exit 0
fi
curl -s -X POST \
-H 'Content-Type: application/json' \
-d "{\"text\": \"[Claude Code] $CLAUDE_NOTIFICATION\"}" \
"$SLACK_WEBHOOK_URL" > /dev/null 2>&1
고급 패턴
조건부 Hook 실행
파일 확장자나 디렉터리에 따라 다른 명령을 실행해야 하는 경우, 셸 스크립트 내에서 조건 분기를 처리합니다:
#!/bin/bash
# scripts/conditional-lint.sh
FILE="$CLAUDE_FILE_PATH"
case "$FILE" in
*.ts|*.tsx)
npx eslint --fix "$FILE" 2>&1
;;
*.py)
python -m ruff check --fix "$FILE" 2>&1
;;
*.go)
gofmt -w "$FILE" 2>&1
;;
*.rs)
rustfmt "$FILE" 2>&1
;;
*)
exit 0
;;
esac
이 스크립트를 하나의 PostToolUse hook으로 등록하면 다국어 프로젝트에서도 파일 유형별로 적절한 린터를 자동 적용할 수 있습니다.
다중 Hook 체이닝
여러 hook을 순차적으로 실행하려면 배열에 순서대로 등록합니다. 앞선 hook이 실패하면(PreToolUse의 경우) 이후 hook은 실행되지 않습니다:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"command": "npx prettier --write $CLAUDE_FILE_PATH"
},
{
"matcher": "Write",
"command": "npx eslint --fix $CLAUDE_FILE_PATH"
},
{
"matcher": "Write",
"command": "npx tsc --noEmit 2>&1 | head -10"
}
]
}
}
이 설정에서는 파일 작성 후 Prettier 포맷팅, ESLint 수정, TypeScript 타입 체크가 순차적으로 실행됩니다.
도구 입력 기반 필터링
$CLAUDE_TOOL_INPUT 환경 변수는 JSON 형식으로 도구의 전체 입력을 제공합니다. jq를 활용하면 입력 내용을 기반으로 정밀한 필터링이 가능합니다:
#!/bin/bash
# scripts/bash-guard.sh
# Bash 도구 실행 시 위험한 명령을 차단합니다
COMMAND=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.command // empty')
DANGEROUS_PATTERNS=(
"rm -rf /"
"DROP DATABASE"
"DROP TABLE"
"> /dev/sda"
"mkfs"
":(){:|:&};:"
)
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "BLOCKED: Dangerous command detected: $pattern"
exit 1
fi
done
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash scripts/bash-guard.sh"
}
]
}
}
로그 수집
모든 도구 호출을 로그 파일에 기록하여 Claude Code의 동작을 감사(audit)하는 hook을 설정할 수 있습니다:
{
"hooks": {
"PreToolUse": [
{
"matcher": "",
"command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) | Tool: $CLAUDE_TOOL_NAME | Input: $CLAUDE_TOOL_INPUT\" >> .claude/hook-audit.log"
}
]
}
}
빈 문자열 ""을 matcher로 사용하면 모든 도구 호출에 대해 hook이 트리거됩니다. 이 로그는 Claude Code가 어떤 작업을 수행했는지 사후 검토할 때 유용합니다.
Git 자동 커밋
Claude Code가 파일을 변경할 때마다 자동으로 git commit을 생성하면 변경 이력을 세밀하게 추적할 수 있습니다:
#!/bin/bash
# scripts/auto-commit.sh
FILE="$CLAUDE_FILE_PATH"
if [ -z "$FILE" ]; then
exit 0
fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
if git diff --quiet "$FILE" 2>/dev/null; then
exit 0
fi
git add "$FILE"
git commit -m "auto: Claude Code modified $FILE" --no-verify 2>/dev/null || true
이 패턴은 실험적인 작업이나 Claude Code의 변경 사항을 세밀하게 추적하고 싶을 때 유용합니다. 다만 프로덕션 프로젝트에서는 커밋 히스토리가 지나치게 많아질 수 있으므로 주의가 필요합니다.
베스트 프랙티스
성능 고려사항
hook은 Claude Code의 응답 속도에 직접적인 영향을 미칩니다. 다음 원칙을 준수하면 체감 성능 저하를 최소화할 수 있습니다.
timeout을 적절히 설정하세요. 기본값인 60초는 대부분의 경우 지나치게 깁니다. 린터나 포맷터는 보통 510초 이내에 완료되므로, timeout을 1000015000(밀리초)으로 설정하면 비정상 상황에서의 대기 시간을 줄일 수 있습니다.
출력량을 제한하세요. hook 명령의 stdout은 Claude에게 전달됩니다. 수백 줄의 출력이 발생하면 컨텍스트 윈도우를 불필요하게 소비합니다. head, tail 등으로 출력을 제한하세요.
불필요한 hook 실행을 피하세요. matcher를 최대한 구체적으로 지정하여, 관련 없는 도구 호출에서 hook이 실행되지 않도록 합니다.
오류 처리 전략
hook의 오류 처리 방식은 목적에 따라 달라져야 합니다.
차단형 hook (PreToolUse): 위험한 동작을 방지하는 것이 목적이므로, 오류 발생 시 명확한 메시지와 함께 exit 1을 반환하세요. Claude는 이 메시지를 읽고 다른 접근 방식을 시도합니다.
정보성 hook (PostToolUse): 포맷팅이나 린팅처럼 실패해도 작업 흐름을 멈출 필요가 없는 경우, || true로 오류를 무시하거나 exit 0으로 항상 성공을 반환하세요.
외부 연동 hook (Notification): 네트워크 오류 등 외부 요인에 의한 실패가 빈번할 수 있습니다. 반드시 || true 또는 > /dev/null 2>&1로 실패를 무시하도록 처리하세요.
보안 고려사항
hook은 시스템 셸에서 실행되므로 보안에 주의해야 합니다.
민감한 정보를 설정 파일에 직접 포함하지 마세요. API 키, Webhook URL 등은 환경 변수로 관리하고, hook 스크립트에서 참조하세요.
프로젝트 레벨 hook을 코드 리뷰에 포함하세요. .claude/settings.json이 버전 관리에 포함되면 팀원 누구나 hook을 수정할 수 있습니다. 악의적이거나 부주의한 hook 변경이 팀 전체에 영향을 미칠 수 있으므로, 이 파일의 변경 사항은 반드시 코드 리뷰를 거쳐야 합니다.
hook에서 신뢰할 수 없는 입력을 처리할 때 주의하세요. $CLAUDE_TOOL_INPUT은 JSON 형식이지만, 셸 명령에 직접 삽입하면 인젝션 취약점이 발생할 수 있습니다. jq로 파싱한 후 사용하는 것이 안전합니다.
팀 협업 패턴
팀 프로젝트에서 hook을 효과적으로 활용하기 위한 권장 패턴입니다.
공통 hook은 프로젝트 레벨에 배치하세요. 코드 포맷팅, 린팅, 파일 보호 등 팀 전체가 동일하게 적용해야 하는 hook은 .claude/settings.json에 정의합니다.
개인 hook은 사용자 레벨에 배치하세요. Slack 알림, 개인 로깅 등 개인 취향에 따른 hook은 ~/.claude/settings.json에 정의합니다.
hook 스크립트는 별도 디렉터리로 관리하세요. scripts/hooks/ 또는 .claude/hooks/ 디렉터리에 스크립트를 모아두면 관리가 용이합니다. 인라인 명령은 간단한 경우에만 사용하고, 복잡한 로직은 스크립트 파일로 분리하세요.
트러블슈팅
Hook이 실행되지 않는 경우
matcher 확인: matcher는 도구 이름과 정확히 일치해야 합니다. Claude Code에서 사용하는 도구 이름은 Write, Read, Bash, Glob, Grep 등이며, 대소문자를 구분합니다. write가 아닌 Write로 지정해야 합니다.
설정 파일 위치 확인: 프로젝트 레벨 설정은 프로젝트 루트의 .claude/settings.json에 있어야 합니다. 파일 경로가 올바른지 확인하세요.
JSON 구문 오류 확인: settings.json에 JSON 구문 오류가 있으면 전체 hook 설정이 무시됩니다. jq . .claude/settings.json 명령으로 JSON 유효성을 검증할 수 있습니다.
Hook이 실패하는 경우
명령어 경로 확인: hook 명령에서 사용하는 실행 파일(예: npx, python, eslint)이 Claude Code의 실행 환경에서 접근 가능한지 확인하세요. 절대 경로를 사용하면 경로 문제를 방지할 수 있습니다.
권한 확인: 스크립트 파일에 실행 권한이 있는지 확인하세요. chmod +x scripts/your-hook.sh로 권한을 부여할 수 있습니다.
timeout 확인: 명령 실행이 timeout을 초과하면 hook이 실패합니다. timeout 값을 늘리거나 명령 자체를 최적화하세요.
디버깅 방법
hook 동작을 디버깅하려면 로그를 남기는 간단한 hook을 추가하세요:
{
"hooks": {
"PreToolUse": [
{
"matcher": "",
"command": "echo \"[DEBUG] Tool: $CLAUDE_TOOL_NAME, File: $CLAUDE_FILE_PATH\" >> /tmp/claude-hooks-debug.log"
}
]
}
}
이 hook은 모든 도구 호출 시 디버그 정보를 로그 파일에 기록합니다. 문제를 파악한 후에는 반드시 제거하세요.
셸 명령 자체를 독립적으로 테스트하는 것도 좋은 방법입니다. 환경 변수를 수동으로 설정한 후 명령을 직접 실행해 보세요:
CLAUDE_FILE_PATH="src/index.ts" CLAUDE_TOOL_NAME="Write" bash scripts/your-hook.sh
echo $? # 종료 코드 확인
흔한 실수
무한 루프 주의: PostToolUse hook에서 파일을 수정하면 다시 Write 도구가 트리거될 수 있습니다. 대부분의 경우 Claude Code가 이를 감지하고 루프를 방지하지만, 복잡한 시나리오에서는 주의가 필요합니다.
환경 변수 미존재: 모든 환경 변수가 항상 제공되는 것은 아닙니다. 예를 들어, $CLAUDE_FILE_PATH는 Bash 도구 호출 시에는 설정되지 않을 수 있습니다. 스크립트에서 변수 존재 여부를 먼저 확인하세요.
Windows 경로 구분자: Windows 환경에서는 $CLAUDE_FILE_PATH에 백슬래시(\)가 포함될 수 있습니다. 크로스 플랫폼 호환성이 필요하면 경로 처리에 주의하세요.
자주 묻는 질문
설정 관련
Q: hook 설정을 변경하면 Claude Code를 재시작해야 하나요?
A: 아니요. settings.json을 저장하면 다음 도구 호출부터 변경된 설정이 적용됩니다. 세션을 재시작할 필요는 없습니다.
Q: 프로젝트 레벨과 사용자 레벨에 같은 matcher의 hook을 등록하면 어떻게 되나요? A: 양쪽 모두 실행됩니다. 프로젝트 레벨 hook이 먼저 실행된 후 사용자 레벨 hook이 실행됩니다. 한쪽에서만 실행하고 싶다면 한 레벨에만 등록하세요.
Q: matcher에 정규식을 사용할 수 있나요?
A: matcher는 도구 이름에 대한 문자열 매칭을 수행합니다. 빈 문자열("")은 모든 도구와 매칭됩니다. 복잡한 패턴 매칭이 필요하면 hook 스크립트 내부에서 $CLAUDE_TOOL_NAME을 확인하세요.
실행 관련
Q: hook 명령의 작업 디렉터리(cwd)는 어디인가요? A: 프로젝트 루트 디렉터리입니다. 상대 경로로 스크립트를 참조할 때 이 점을 기준으로 하세요.
Q: hook에서 비동기 작업을 실행할 수 있나요?
A: hook 명령 끝에 &를 붙여 백그라운드로 실행할 수 있지만, Claude Code는 해당 명령의 완료를 기다리지 않으므로 결과를 확인할 수 없습니다. 알림 전송 등 결과 확인이 불필요한 작업에만 사용하세요.
Q: hook에서 실행한 명령의 stdout은 어디로 가나요? A: Claude에게 전달됩니다. Claude는 hook의 출력을 읽고 그에 따라 행동을 조정합니다. PreToolUse hook에서 차단 사유를 출력하면 Claude가 이를 이해하고 다른 방법을 시도합니다.
호환성 관련
Q: Windows에서도 hook이 작동하나요? A: 작동합니다. Windows에서 Claude Code는 기본적으로 Git Bash나 WSL 환경의 셸을 사용합니다. 순수 PowerShell 환경에서는 셸 명령 구문이 다를 수 있으므로, bash 호환 스크립트를 사용하는 것을 권장합니다.
Q: Docker 컨테이너 안에서 Claude Code를 실행할 때 hook이 작동하나요? A: 컨테이너 내부에 hook 명령에 필요한 도구(린터, 포맷터 등)가 설치되어 있다면 정상적으로 작동합니다. Dockerfile에 필요한 도구를 포함시키세요.
Q: 원격 서버에서 Claude Code를 사용할 때 hook에서 로컬 도구를 호출할 수 있나요? A: hook은 Claude Code가 실행되는 환경의 셸에서 실행됩니다. 원격 서버에서 Claude Code를 실행하면 hook도 원격 서버에서 실행됩니다. 로컬 도구를 호출하려면 SSH 터널이나 API 호출을 통해 간접적으로 연동해야 합니다.
마무리
Claude Code hooks는 단순한 자동화 도구를 넘어, Claude Code를 팀의 개발 워크플로우에 깊이 통합하는 인터페이스입니다. 코드 품질 검증, 보안 가드레일, 외부 서비스 연동까지 다양한 시나리오에 적용할 수 있습니다.
처음에는 자동 포맷팅이나 간단한 린팅 hook부터 시작하세요. hook의 동작 방식과 디버깅 방법에 익숙해지면 점진적으로 파일 보호, 조건부 실행, 외부 알림 등 고급 패턴으로 확장할 수 있습니다. 설정 파일 하나로 시작하는 작은 자동화가 결국 팀 전체의 개발 생산성을 높이는 기반이 됩니다.