[Claude Code 마스터하기 #2] Hooks & Headless Mode: 반복 작업을 자동화하는 법
이 글은 Claude Code 시리즈의 두 번째 편 입니다. Part 1에서 기본 기능을 익혔다면, 이제 자동화로 넘어갑니다.
TL;DR
- Hooks: 특정 이벤트 발생 시 자동으로 스크립트 실행
- Headless Mode: 대화 없이 명령 실행 후 결과만 받기
- 조합하면 CI/CD, pre-commit, 자동 포맷팅 등 무한 자동화 가능
들어가며
Part 1에서 Claude Code의 기본 사용법을 익혔습니다. 하지만 매번 같은 작업을 반복하고 있진 않나요?
파일 수정 → ruff format 실행 → 잊어버림 → 린트 에러
파일 수정 → ruff format 실행 → 잊어버림 → 린트 에러
(무한 반복)
이런 반복을 자동화하는 두 가지 강력한 기능이 있습니다.
| 기능 | 비유 | 용도 |
|---|---|---|
| Hooks | 게임 매크로 | 이벤트 → 자동 실행 |
| Headless Mode | 쉘 스크립트 | 대화 없이 한 번 실행 |
Part A: Hooks (훅)
Hooks란?
"특정 이벤트가 발생하면 자동으로 스크립트를 실행해라"
게임에서 매크로를 설정하는 것과 비슷합니다:
- 적이 나타나면 → 자동으로 스킬 사용
- 체력이 30% 이하면 → 자동으로 포션 사용
Claude Code에서는:
- 파일 수정하면 → 자동으로 포맷팅
- Bash 실행 전 → 위험 명령 차단
- 작업 완료하면 → 자동으로 커밋
Hook 이벤트 8가지
| 이벤트 | 발생 시점 | 주요 용도 |
|---|---|---|
PreToolUse | 도구 실행 직전 | 위험 명령 차단, 파일 보호 |
PostToolUse | 도구 실행 직후 | 자동 포맷팅, 로깅 |
Notification | 알림 발생 시 | 슬랙/디스코드 알림 |
Stop | 작업 완료 시 | 자동 커밋, 리포트 생성 |
SubagentStop | 서브에이전트 완료 시 | 결과 후처리 |
PreCompact | 대화 압축 직전 | 중요 정보 백업 |
가장 많이 쓰는 3개: PreToolUse, PostToolUse, Stop
Hook 설정 파일
~/.claude/settings.json # 전역 (모든 프로젝트)
.claude/settings.json # 프로젝트 (팀 공유)
.claude/settings.local.json # 로컬 전용 (gitignore)
Hook 기본 구조
{
"hooks": {
"이벤트명": [
{
"matcher": "도구명 또는 패턴",
"command": "실행할 명령어"
}
]
}
}
Matcher 패턴
| Matcher | 의미 |
|---|---|
Edit | 파일 수정 (str_replace_editor) |
Write | 파일 생성/덮어쓰기 |
Read | 파일 읽기 |
Bash | 쉘 명령 실행 |
Edit|Write | Edit 또는 Write |
* | 모든 도구 |
Exit Code 의미
| Exit Code | 의미 | 동작 |
|---|---|---|
| 0 | 성공 | 계속 진행 |
| 2 | 차단 | 도구 실행 중단, stderr 메시지 전달 |
| 그 외 | 에러 | 에러 로깅 후 계속 진행 |
실전 Hook 예제 5가지
예제 1: Python 파일 자동 포맷팅
시나리오: Claude가 Python 파일을 수정할 때마다 자동으로 ruff format 실행
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "python3 ~/.claude/hooks/auto_format.py \"$CLAUDE_FILE_PATHS\""
}
]
}
}
~/.claude/hooks/auto_format.py:
#!/usr/bin/env python3
import subprocess
import sys
file_paths = sys.argv[1] if len(sys.argv) > 1 else ""
for path in file_paths.split(":"):
path = path.strip()
if path.endswith(".py"):
subprocess.run(["ruff", "format", path], capture_output=True)
subprocess.run(["ruff", "check", "--fix", path], capture_output=True)
결과:
Claude: src/utils.py 파일을 수정했습니다.
(자동으로 ruff format + ruff check --fix 실행)
예제 2: 민감 파일 보호
시나리오: .env, secrets.py 등 민감 파일 수정 시도 시 차단
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|Read",
"command": "python3 ~/.claude/hooks/protect_sensitive.py \"$CLAUDE_FILE_PATHS\""
}
]
}
}
~/.claude/hooks/protect_sensitive.py:
#!/usr/bin/env python3
import sys
import os
PROTECTED_PATTERNS = [
".env",
"secrets.py",
"credentials",
".pem",
"id_rsa",
"private_key"
]
file_paths = sys.argv[1] if len(sys.argv) > 1 else ""
for path in file_paths.split(":"):
path = path.strip().lower()
for pattern in PROTECTED_PATTERNS:
if pattern in path:
print(f"🚫 보호된 파일입니다: {path}", file=sys.stderr)
print(f"민감한 정보가 포함된 파일은 직접 수정하세요.", file=sys.stderr)
sys.exit(2) # Exit code 2 = 차단
sys.exit(0) # 통과
결과:
나: .env 파일에 새 API 키 추가해줘
Claude: 🚫 보호된 파일입니다: .env
민감한 정보가 포함된 파일은 직접 수정하세요.
예제 3: 위험 명령어 차단
시나리오: rm -rf, sudo 등 위험한 Bash 명령 차단
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "python3 ~/.claude/hooks/block_dangerous.py \"$CLAUDE_TOOL_INPUT\""
}
]
}
}
~/.claude/hooks/block_dangerous.py:
#!/usr/bin/env python3
import sys
import re
import json
DANGEROUS_PATTERNS = [
r"rm\s+-rf\s+[/~]", # rm -rf / 또는 ~
r"rm\s+-rf\s+\*", # rm -rf *
r"sudo\s+", # 모든 sudo
r"chmod\s+777", # 과도한 권한
r">\s*/dev/sd", # 디스크 직접 쓰기
r"mkfs\.", # 포맷
r"dd\s+if=", # 디스크 복사
r":\(\)\{.*\}", # fork bomb
]
try:
tool_input = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
command = tool_input.get("command", "")
except:
command = sys.argv[1] if len(sys.argv) > 1 else ""
for pattern in DANGEROUS_PATTERNS:
if re.search(pattern, command, re.IGNORECASE):
print(f"⛔ 위험한 명령어가 감지되었습니다!", file=sys.stderr)
print(f"명령어: {command}", file=sys.stderr)
print(f"매칭 패턴: {pattern}", file=sys.stderr)
sys.exit(2)
sys.exit(0)
결과:
나: 임시 파일 정리해줘
Claude: (rm -rf /tmp/* 시도)
⛔ 위험한 명령어가 감지되었습니다!
명령어: rm -rf /tmp/*
더 안전한 방법으로 시도하겠습니다...
예제 4: 작업 완료 시 자동 커밋
시나리오: Claude 작업이 끝나면 자동으로 git commit
{
"hooks": {
"Stop": [
{
"command": "bash ~/.claude/hooks/auto_commit.sh"
}
]
}
}
~/.claude/hooks/auto_commit.sh:
#!/bin/bash
# Git 저장소인지 확인
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
exit 0
fi
# 변경사항이 있는지 확인
if git diff --quiet && git diff --cached --quiet; then
exit 0
fi
# 변경된 파일 목록
changed_files=$(git diff --name-only)
# 자동 커밋
git add -A
git commit -m "chore: Claude Code 자동 커밋
변경된 파일:
$changed_files
[auto-committed by Claude Code hook]"
echo "✅ 자동 커밋 완료"
결과:
Claude: 리팩토링을 완료했습니다. 3개 파일을 수정했습니다.
(자동으로 git commit 실행)
✅ 자동 커밋 완료
예제 5: 세션 시작 시 컨텍스트 로드
시나리오: Claude Code 시작할 때 현재 상태를 자동으로 보여주기
{
"hooks": {
"SessionStart": [
{
"command": "bash ~/.claude/hooks/session_context.sh"
}
]
}
}
~/.claude/hooks/session_context.sh:
#!/bin/bash
echo "📋 현재 프로젝트 상태"
echo "===================="
# Git 상태
if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
echo ""
echo "🔀 Git 브랜치: $(git branch --show-current)"
uncommitted=$(git status --porcelain | wc -l)
if [ "$uncommitted" -gt 0 ]; then
echo "📝 미커밋 변경: ${uncommitted}개 파일"
fi
fi
# TODO 항목
if [ -f "TODO.md" ]; then
echo ""
echo "📌 TODO 항목:"
grep -E "^- \[ \]" TODO.md | head -5
fi
# 최근 에러 로그
if [ -f "logs/error.log" ]; then
recent_errors=$(tail -1 logs/error.log 2>/dev/null)
if [ -n "$recent_errors" ]; then
echo ""
echo "⚠️ 최근 에러:"
echo "$recent_errors"
fi
fi
echo ""
echo "===================="
결과:
$ claude
📋 현재 프로젝트 상태
====================
🔀 Git 브랜치: feature/user-auth
📝 미커밋 변경: 2개 파일
📌 TODO 항목:
- [ ] 로그인 API 테스트 작성
- [ ] 비밀번호 재설정 기능
⚠️ 최근 에러:
2025-01-15 10:23:45 ERROR: Database connection timeout
====================
> 무엇을 도와드릴까요?
Hook 환경 변수
Hook 스크립트에서 사용할 수 있는 환경 변수:
| 변수 | 설명 | 예시 |
|---|---|---|
CLAUDE_TOOL_NAME | 실행된 도구 이름 | Edit, Bash |
CLAUDE_TOOL_INPUT | 도구 입력 (JSON) | {"command": "ls"} |
CLAUDE_FILE_PATHS | 대상 파일 경로 | src/main.py:src/utils.py |
CLAUDE_PROJECT_DIR | 프로젝트 루트 | /home/user/project |
CLAUDE_SESSION_ID | 세션 ID | abc123 |
Part B: Headless Mode
Headless Mode란?
"대화 없이 명령 실행하고 결과만 받기"
일반 모드:
$ claude
> 안녕
Claude: 안녕하세요! 무엇을 도와드릴까요?
> (대화 계속...)
Headless 모드:
$ claude -p "이 파일의 버그를 찾아줘" --output-format json
{"result": "버그를 발견했습니다...", "cost": 0.003}
대화형 UI 없이 결과만 받습니다. 스크립트, CI/CD, 자동화에 필수입니다.
기본 사용법
# 기본 (텍스트 출력)
claude -p "프롬프트"
# JSON 출력
claude -p "프롬프트" --output-format json
# 스트리밍 JSON
claude -p "프롬프트" --output-format stream-json
# 모델 지정
claude -p "프롬프트" --model claude-sonnet-4-5-20250929
# 최대 턴 수 제한
claude -p "프롬프트" --max-turns 5
# 권한 모드
claude -p "프롬프트" --permission-mode accept-edits
권한 모드 옵션
| 모드 | 의미 | 용도 |
|---|---|---|
default | 모든 작업 확인 요청 | 안전한 기본값 |
accept-edits | 파일 수정 자동 승인 | 자동화 스크립트 |
bypass-permissions | 모든 작업 자동 승인 | CI/CD (주의!) |
