Claude Code 훅이란 무엇인가?
Claude Code를 사용하다 보면 반복적으로 해야 하는 일들이 생깁니다. 파일을 수정할 때마다 포맷팅을 실행해야 한다거나, 커밋 전에 반드시 테스트를 돌려야 한다거나, 위험한 명령어가 실행될 때 경고를 받고 싶다거나 하는 것들입니다. 이런 자동화를 가능하게 하는 것이 바로 훅(Hook) 시스템입니다.
훅은 Claude Code가 특정 동작을 수행하는 시점에 자동으로 실행되는 셸 명령어입니다. 예를 들어 Claude가 파일을 수정할 때마다 자동으로 ESLint를 실행하거나, Claude가 작업을 완료할 때 빌드를 검증하는 식으로 활용할 수 있습니다.
훅은 Claude가 아닌 Claude Code 하네스(harness)가 실행합니다. 즉, 훅 명령어는 Claude의 판단과 무관하게 항상 실행됩니다. 이 점이 훅을 신뢰할 수 있는 자동화 수단으로 만드는 핵심입니다.
훅의 종류 3가지 — PreToolUse, PostToolUse, Stop
Claude Code는 세 가지 타입의 훅을 제공합니다. 각각 실행 시점이 다릅니다.
| 훅 타입 | 실행 시점 | 주요 용도 |
|---|---|---|
| PreToolUse | 도구 실행 전 | 위험 명령어 차단, 입력 검증 |
| PostToolUse | 도구 실행 후 | 자동 포맷팅, 린트, 테스트 실행 |
| Stop | 세션 종료 시 | 최종 빌드 검증, 보고서 생성 |
PreToolUse는 Claude가 어떤 도구(파일 쓰기, 명령어 실행 등)를 사용하기 전에 실행됩니다. 종료 코드 2를 반환하면 해당 도구 실행을 차단할 수 있어 안전장치로 활용하기 좋습니다.
PostToolUse는 도구 실행이 완료된 후 실행됩니다. 파일 저장 후 자동 포맷팅, 코드 린트, 타입 검사 등에 이상적입니다.
Stop은 Claude가 세션을 종료할 때 실행됩니다. 프로젝트 빌드 최종 검증, 테스트 전체 실행, 변경 사항 요약 생성 등에 활용합니다.
훅 설정 방법 — settings.json 구조
훅은 ~/.claude/settings.json 파일에서 설정합니다. 프로젝트별로 다르게 설정하고 싶다면 프로젝트 루트의 .claude/settings.json을 사용합니다. 프로젝트 설정이 글로벌 설정보다 우선합니다.
기본 구조는 다음과 같습니다:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "명령어",
"description": "훅 설명"
}
],
"PreToolUse": [
{
"matcher": "Bash",
"command": "검증 명령어",
"description": "훅 설명"
}
],
"Stop": [
{
"command": "빌드 명령어",
"description": "훅 설명"
}
]
}
}
matcher는 정규식 패턴으로, 어떤 도구에 훅을 적용할지 지정합니다. Write|Edit는 파일 쓰기와 편집 도구 모두에 적용하며, Bash는 터미널 명령어 실행에, .*는 모든 도구에 적용합니다.
훅 명령어는 환경변수 $FILE_PATH로 현재 처리 중인 파일 경로를 받을 수 있습니다. 이를 활용하면 해당 파일에만 린트나 포맷팅을 적용하는 효율적인 훅을 만들 수 있습니다.
PreToolUse 훅 실전 활용 — 파일 저장 전 린트, 위험 명령어 차단
활용 1: 대용량 파일 저장 차단
800줄을 넘는 파일은 저장하지 못하도록 차단하는 훅입니다. 코딩 스타일 규칙을 자동으로 강제합니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[훅] 차단: 파일이 800줄을 초과합니다 ('+lines+'줄)');process.exit(2)}console.log(d)})\"",
"description": "800줄 초과 파일 저장 차단"
}
]
}
}
활용 2: 위험 명령어 차단
rm -rf, git push --force 같은 위험한 명령어가 실행되기 전에 경고하고 차단합니다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash -c 'read input; echo \"$input\" | python3 -c \"import sys,json; d=json.load(sys.stdin); cmd=d.get(\\\"tool_input\\\",{}).get(\\\"command\\\",\\\"\\\"); dangerous=[\\\"rm -rf\\\",\\\"git push --force\\\",\\\"DROP TABLE\\\"]; [sys.exit(2) or print(\\\"[훅] 위험 명령어 차단:\\\",p) for p in dangerous if p in cmd]; print(json.dumps(d))\"'",
"description": "위험한 명령어 실행 차단"
}
]
}
}
PostToolUse 훅 실전 활용 — 자동 포맷팅, 테스트 실행
활용 3: 파일 저장 후 자동 Prettier 포맷팅
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "pnpm prettier --write \"$FILE_PATH\" 2>/dev/null || true",
"description": "파일 저장 후 자동 포맷팅"
}
]
}
}
활용 4: TypeScript 파일 편집 후 타입 검사
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "if [[ \"$FILE_PATH\" == *.ts || \"$FILE_PATH\" == *.tsx ]]; then pnpm tsc --noEmit --pretty false 2>&1 | tail -20; fi",
"description": "TypeScript 파일 편집 후 타입 검사"
}
]
}
}
활용 5: ESLint 자동 실행 및 수정
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "pnpm eslint --fix \"$FILE_PATH\" 2>/dev/null || true",
"description": "ESLint 자동 수정"
}
]
}
}
활용 6: Python 파일 black 포맷팅
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "if [[ \"$FILE_PATH\" == *.py ]]; then black \"$FILE_PATH\" 2>/dev/null; fi",
"description": "Python 파일 black 포맷팅"
}
]
}
}
Stop 훅 활용 — 세션 종료 시 빌드 검증
Stop 훅은 Claude가 작업을 완료하고 세션을 종료할 때 실행됩니다. 최종 빌드가 성공하는지 확인하는 용도로 가장 많이 씁니다.
활용 7: 세션 종료 시 프로덕션 빌드 검증
{
"hooks": {
"Stop": [
{
"command": "pnpm build 2>&1 | tail -30",
"description": "세션 종료 시 프로덕션 빌드 검증"
}
]
}
}
활용 8: 테스트 전체 실행
{
"hooks": {
"Stop": [
{
"command": "pnpm test --passWithNoTests 2>&1 | tail -40",
"description": "세션 종료 시 전체 테스트 실행"
}
]
}
}
훅 10가지 실전 예제 코드
예제 9: CSS/SCSS 파일 Stylelint 자동 실행
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "if [[ \"$FILE_PATH\" == *.css || \"$FILE_PATH\" == *.scss ]]; then pnpm stylelint --fix \"$FILE_PATH\" 2>/dev/null || true; fi",
"description": "CSS/SCSS 파일 Stylelint 자동 실행"
}
]
}
}
예제 10: 완전한 프론트엔드 훅 설정 (포맷 → 린트 → 타입 검사)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "pnpm prettier --write \"$FILE_PATH\" 2>/dev/null || true",
"description": "1단계: 자동 포맷팅"
},
{
"matcher": "Write|Edit",
"command": "pnpm eslint --fix \"$FILE_PATH\" 2>/dev/null || true",
"description": "2단계: ESLint 자동 수정"
},
{
"matcher": "Write|Edit",
"command": "if [[ \"$FILE_PATH\" == *.ts || \"$FILE_PATH\" == *.tsx ]]; then pnpm tsc --noEmit --pretty false 2>&1 | head -20; fi",
"description": "3단계: TypeScript 타입 검사"
}
],
"PreToolUse": [
{
"matcher": "Write",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';if(c.split('\\n').length>800){process.stderr.write('[훅] 800줄 초과 차단\\n');process.exit(2)}process.stdout.write(d)})\"",
"description": "파일 크기 제한"
}
],
"Stop": [
{
"command": "pnpm build 2>&1 | tail -20",
"description": "최종 빌드 검증"
}
]
}
}
팁: 훅 명령어가 실패해도 Claude Code의 기본 동작에는 영향을 주지 않습니다 (PreToolUse 훅에서 exit 2를 반환하는 경우 제외). 따라서 훅에 || true를 붙여 에러를 무시하는 것이 좋은 습관입니다.
훅 작성 시 주의사항
훅을 작성할 때 몇 가지 중요한 점을 기억해야 합니다.
- 무한 루프 주의: PostToolUse 훅에서 파일을 수정하면 다시 PostToolUse 훅이 트리거될 수 있습니다. Prettier나 ESLint처럼 같은 파일을 두 번 처리해도 결과가 같은 도구만 사용하세요.
- 성능 고려: 모든 파일 저장에 무거운 타입 검사를 실행하면 속도가 느려집니다. 파일 확장자로 필터링하거나, Stop 훅으로 옮기는 것을 고려하세요.
- 경로 처리:
$FILE_PATH는 절대 경로로 전달됩니다. 상대 경로가 필요하다면 변환이 필요합니다. - 에러 출력: 훅에서 출력되는 내용은 Claude에게 컨텍스트로 전달될 수 있습니다. 중요한 에러는 stderr로 출력하는 것이 좋습니다.
- 환경 변수: 훅 명령어는 Claude Code가 실행된 셸 환경을 상속합니다. PATH, 가상환경 등이 올바르게 설정되어 있는지 확인하세요.
훅 시스템을 잘 활용하면 Claude Code가 단순한 AI 어시스턴트를 넘어 팀의 코딩 표준을 자동으로 강제하는 시스템으로 변합니다. 한 번 설정하면 모든 팀원에게 일관된 코드 품질을 보장할 수 있습니다.