Hooks 코스의 첫 글입니다. 훅은 "무엇이 일어나면 자동으로 이 명령을 실행하라"는 장치이고, 우리 교육에서는 "프롬프트는 부탁, 훅은 강제"로 소개합니다. CLAUDE.md의 규칙은 모델이 잊을 수 있지만, 훅으로 건 검사는 매번 반드시 돌아갑니다. 설정 구조, echo 한 줄 훅으로 동작 확인, 에러가 났을 때 볼 곳까지 다룹니다. 다음 글부터는 이벤트별 실전 예로 이어집니다.
핵심 요약
- 훅은 세 겹으로 적습니다. 이벤트 이름 → matcher 그룹 → 핸들러(
type,command등). - 설정 위치는
~/.claude/settings.json(전역),.claude/settings.json(프로젝트, 커밋 가능),.claude/settings.local.json(개인용)입니다. /hooks는 읽기 전용 메뉴입니다. 등록 여부와 출처를 확인하는 용도이고, 추가·수정은 JSON을 직접 고칩니다.- 명령은
.claude/hooks/*.sh로 떼어내고"$CLAUDE_PROJECT_DIR"경로로 부르면 이스케이프 지옥에서 벗어납니다. - 에러는 트랜스크립트의
<이벤트명> hook error한 줄과 디버그 로그, 그리고 스크립트를 수동 실행해 종료 코드를 확인하는 세 곳에서 찾습니다.
settings.json의 기본 구조
훅 설정은 hooks 키 아래에 놓입니다. 구조는 항상 같습니다. 바깥에서 안쪽으로 세 단계입니다.
- 이벤트 이름:
PreToolUse,PostToolUse,SessionStart,Stop,UserPromptSubmit,SubagentStop,Notification같은 수명 주기 지점입니다. - matcher 그룹: 그 이벤트 중 어떤 경우에만 반응할지 거르는 필터입니다. 도구 이벤트라면 도구 이름(
Bash,Edit|Write), SessionStart라면 시작 방식(startup,compact)을 적습니다. 비우거나"*"로 두면 전부에 반응합니다. - 핸들러 배열
hooks: 실제로 실행할 것입니다.type은command(셸 명령),http(URL로 POST),mcp_tool(연결된 MCP 도구 호출),prompt(모델에게 예/아니오 판단),agent(도구를 쓰는 서브에이전트 검증, 실험적) 다섯 가지이며 대부분은command를 씁니다.
가장 짧은 형태는 이렇습니다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "echo 'file edited' >> ~/.claude/edit.log",
"timeout": 30
}
]
}
]
}
}
핸들러에 붙일 수 있는 공통 옵션은 timeout(초 단위, command 기본 600초, 단 UserPromptSubmit은 30초), if(도구 이벤트에서 "Bash(git *)" 같은 권한 규칙 문법으로 한 번 더 거르기), statusMessage(실행 중 스피너에 보일 문구)입니다. command 타입에는 추가로 async(백그라운드 실행), args(셸 없이 직접 실행하는 exec 형식), shell(bash 또는 powershell)이 있습니다.
설정 파일 위치에 따라 범위가 달라집니다.
| 위치 | 범위 | 공유 |
|---|---|---|
~/.claude/settings.json |
이 컴퓨터의 모든 프로젝트 | 안 됨(개인 머신 전용) |
.claude/settings.json |
해당 프로젝트 | 됨(레포에 커밋) |
.claude/settings.local.json |
해당 프로젝트, 나만 | 안 됨(gitignore 대상) |
플러그인 hooks/hooks.json |
플러그인 활성화 동안 | 플러그인과 함께 배포 |
| 스킬·서브에이전트 frontmatter | 그 컴포넌트가 활성일 때만 | 파일 안에 정의 |
Windows에서는 전역 파일이 %USERPROFILE%\.claude\settings.json이고, 셸 형식 명령은 Git Bash로 실행됩니다(Git Bash가 없으면 PowerShell). 이 코스의 예제는 macOS 기준이며, Windows에서는 Git Bash를 설치해 두면 그대로 따라올 수 있습니다.
이미 hooks 키가 있으면 기존 이벤트 키 옆에 형제로 추가합니다. 후행 쉼표나 주석은 JSON 파싱 실패로 훅 전체를 사라지게 합니다.
/hooks 커맨드로 무엇을 확인할 수 있나
Claude Code 안에서 /hooks를 입력하면 등록된 훅의 브라우저가 열립니다. 이벤트별로 훅 개수가 보이고, 이벤트를 고르면 matcher 단위로 내려가 각 핸들러의 세부(이벤트, matcher, 타입, 출처 파일, 전체 명령이나 프롬프트, URL)를 볼 수 있습니다.
출처는 접두사로 표시됩니다. [User]는 ~/.claude/settings.json, [Project]는 .claude/settings.json, [Local]은 settings.local.json, [Plugin]은 플러그인, [Session]은 현재 세션에서 메모리에 등록된 것, [Built-in]은 Claude Code가 내부적으로 등록한 것입니다. "분명히 껐는데 훅이 도는" 혼란은 대개 전역 파일이나 플러그인에 같은 훅이 하나 더 있어서이고, 출처 접두사를 보면 바로 풀립니다.
한 가지 분명히 알아 둘 점은 이 메뉴가 읽기 전용이라는 것입니다. 여기서 훅을 추가하거나 고칠 수는 없습니다. JSON을 직접 편집하거나, Claude에게 "이런 훅을 settings.json에 추가해 줘"라고 요청하면 됩니다. 프로젝트·로컬 설정 파일의 변경은 파일 감시자가 자동으로 반영하므로 대개 재시작 없이 /hooks에 나타납니다. 몇 초가 지나도 안 보이면 JSON 문법을 먼저 의심하고, 그래도 안 되면 세션을 다시 시작합니다. 전역 사용자 설정과 플러그인 활성화 변경은 새 세션에서 반영됩니다.
명령을 sh 파일로 떼어내는 이유
command 문자열에 파이프, 따옴표, jq 필터가 섞이기 시작하면 JSON 안의 이스케이프가 급격히 지저분해집니다. 큰따옴표를 \"로, 백슬래시를 \\로 적어야 하고, 한 글자만 틀려도 훅 전체가 파싱 실패로 사라집니다. 그래서 우리 교육에서는 처음부터 규칙을 하나 정합니다. 훅 명령은 스크립트 파일로 두고, settings.json에는 경로만 적는다.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.sh"
}
]
}
]
}
}
장점은 셋입니다. 이스케이프가 사라지고, 샘플 JSON을 파이프해 단독 테스트가 되며, .claude/hooks/를 커밋하면 팀 전체가 같은 검사를 공유합니다. 우리가 운영하는 훅(인박스 확인, 압축 후 규칙 재주입, 호스트 가드)도 전부 이 방식입니다.
경로는 $CLAUDE_PROJECT_DIR(프로젝트 루트, 모든 훅에서 자동 설정)을 큰따옴표로 감싸 적고, 스크립트에는 실행 권한을 줍니다(chmod +x). "args": []를 붙이면 셸 없이 실행되는 exec 형식이 되어 따옴표가 필요 없습니다.
첫 훅 만들기: echo 로 동작 확인
가장 작은 훅으로 흐름 전체를 한 번 통과해 봅니다. 목표는 "파일이 편집될 때마다 로그 한 줄이 남는가"입니다.
- 스크립트를 만듭니다.
mkdir -p .claude/hooks
cat > .claude/hooks/first-hook.sh <<'EOF'
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // "unknown"')
echo "$(date '+%H:%M:%S') edited: $FILE" >> "$HOME/.claude/first-hook.log"
exit 0
EOF
chmod +x .claude/hooks/first-hook.sh
.claude/settings.json에 등록합니다.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/first-hook.sh"
}
]
}
]
}
}
/hooks를 열어PostToolUse아래에[Project]훅이 보이는지 확인합니다.- Claude에게 "
hello.txt파일을 만들어 줘"라고 요청한 뒤cat ~/.claude/first-hook.log로 줄이 늘어났는지 봅니다.
이것이 "정지 규칙은 증거"입니다. 로그에 줄이 찍힌 화면까지 봐야 끝입니다. jq는 macOS brew install jq, Windows는 jq 배포본 설치 또는 Python 파싱으로 대체합니다.
훅 에러 메시지가 뜨면 어디를 보나
훅이 0이 아닌 코드로 끝나면 트랜스크립트에 PostToolUse:Write hook error 같은 알림과 stderr 첫 줄이 표시됩니다. 대부분 작업 자체는 계속 진행되고(종료 코드 2만 차단), 원인은 다음 순서로 좁힙니다.
- 스크립트 단독 실행: 샘플 JSON을 넣어 종료 코드를 봅니다.
echo '{"tool_name":"Write","tool_input":{"file_path":"/tmp/a.txt"}}' | .claude/hooks/first-hook.sh
echo $?
- command not found: 경로가 상대 경로이거나 실행 권한이 없는 경우입니다.
$CLAUDE_PROJECT_DIR절대 경로와chmod +x를 확인합니다.jq: command not found면 jq 설치 문제입니다. - JSON 검증 실패: 셸 프로필(
~/.zshrc등)이 시작할 때echo를 찍으면 그 문자열이 훅 출력 앞에 붙어 JSON 파싱이 깨집니다. 프로필의 echo를if [[ $- == *i* ]]로 감싸 대화형 셸에서만 찍게 합니다. - 디버그 로그: 매칭된 훅, 종료 코드, 전체 stdout/stderr가 필요하면
claude --debug-file /tmp/claude.log로 시작하고 다른 터미널에서tail -f /tmp/claude.log를 봅니다. 이미 실행 중이면/debug로 로깅을 켤 수 있습니다.Ctrl+O로 트랜스크립트 보기를 켜면 발생한 훅마다 한 줄 요약이 보입니다.
정리
- 훅 설정은 이벤트 → matcher → 핸들러 세 겹이며, 파일 위치가 곧 적용 범위입니다.
/hooks는 확인용입니다. 출처 접두사(User,Project,Local,Plugin)를 보면 "누가 이 훅을 걸었나"가 바로 풀립니다.- 명령은 sh 파일로 떼어내고
"$CLAUDE_PROJECT_DIR"경로만 적습니다. 이스케이프가 사라지고 단독 테스트가 가능해집니다. - 첫 훅은
echo로그 한 줄로 시작해 로그 파일이 늘어나는 것까지 확인합니다. - 에러는 수동 실행 → 경로·권한 → 셸 프로필 출력 → 디버그 로그 순서로 좁힙니다.
자주 묻는 질문
/hooks 커맨드로 훅을 추가하거나 수정할 수 있나요?
없습니다. /hooks는 등록된 훅을 이벤트별로 보여 주고 출처와 명령을 확인하는 읽기 전용 메뉴입니다. 추가·수정·삭제는 해당 설정 JSON을 직접 편집하거나 Claude에게 편집을 요청합니다. 프로젝트·로컬 설정 변경은 대개 자동 반영되고, 전역 설정 변경은 새 세션에서 적용됩니다.
훅 명령을 settings.json에 직접 적지 않고 sh 파일로 빼는 이유는 무엇인가요?
JSON 문자열 안에서 따옴표와 백슬래시를 이스케이프하다 보면 한 글자 실수로 훅 전체가 파싱 실패로 사라지기 때문입니다. 파일로 빼면 셸 문법을 그대로 쓰고, 터미널에서 샘플 JSON을 파이프해 단독 테스트할 수 있으며, .claude/hooks/를 커밋해 팀과 공유할 수 있습니다.
PostToolUse:Write hook error 메시지가 뜨는데 원인은 무엇인가요?
Write 도구 뒤에 걸린 훅 스크립트가 0이 아닌 코드로 끝났다는 뜻입니다. 뒤에 붙는 stderr 첫 줄이 힌트입니다. 경로 오류(command not found), 실행 권한 없음, jq 미설치, 셸 프로필의 echo 출력이 JSON을 깨뜨리는 경우가 대부분입니다. 샘플 JSON을 파이프해 스크립트를 직접 실행하고 echo $?로 종료 코드를 보면 바로 좁혀집니다.
훅 핸들러의 type 에는 어떤 종류가 있나요?
command(셸 명령), http(이벤트 JSON을 URL로 POST), mcp_tool(이미 연결된 MCP 서버의 도구 호출), prompt(모델에게 단일 턴으로 예/아니오 판단을 맡김), agent(Read·Grep 같은 도구를 쓰는 서브에이전트로 검증, 실험적) 다섯 가지입니다. 결정론적 규칙은 command, 판단이 필요한 검사는 prompt로 나눠 쓰는 것이 기본입니다.