이 페이지의 목차
이 페이지의 목차
확인 날짜 안내
공식 훅 가이드 Claude Code 훅 가이드(원본 날짜 미상, 수집 2026-10-18)의 문제 해결 절을 증상별로 정리했다. 7절은 공식 Hooks reference Claude Code 훅 레퍼런스(원본 날짜 미상, 수집 2026-10-18)가 더하는 점검이다. 훅을 처음 만드는 법은 Claude Code 훅 설정 레시피.
증상별 점검표#
| 증상 | 먼저 볼 것 |
|---|---|
| 설정했는데 한 번도 안 돈다 | /에 맞는 이벤트 아래 있는지, matcherPreToolUse는 실행 전, PostToolUse는 실행 후) |
| 트랜스크립트에 "PreToolUse hook error: ..." | 스크립트를 샘플 JSON으로 직접 돌려 종료 코드 확인, 경로·jq· |
/에 아무 훅도 없다 | JSON 문법allow |
| Stop 훅 때문에 멈추지 않다가 경고로 끝난다 | 블록 상한 8회, stop_hook_active 확인 |
| JSON을 냈는데 효과가 없고 오류도 없다 | JSON 앞의 셸 프로필 출력, 필드 층 위치 |
| 훅이 무엇을 했는지 모르겠다 | Ctrl+O 트랜스크립트, 디버그 로그 |
| 정책 훅이 있는데 막혀야 할 동작이 진행된다 | 종료 코드 1을 썼는지, 스크립트 경로 오타·onFailure (7절) |
| 훅이 느리다 | 디버그 로그의 Hooks: 소요 시간 줄 (7절) |
| async 훅 결과가 안 보인다 | 다음 턴 전달, 완료 알림은 verbose에서만 (7절) |
근거: Claude Code 훅 가이드, 아래 세 행은 Claude Code 훅 레퍼런스
1. 훅이 안 돈다#
/hooks를 실행해 맞는 이벤트 아래에 훅이 보이는지 확인한다.- matcher가 도구 이름과 정확히 맞는지 본다. matcher는 대소문자를 구분한다.
- 맞는 이벤트를 일으키고 있는지 본다.
PreToolUse는 도구 실행 전,PostToolUse는 실행 후에 돈다.PermissionRequest는 Claude Code가 권한을 물으려 할 때 돈다.-p비대화형 모드에서는dontAsk모드가 아니면 실행되고, 어떤 훅도 정하지 않고 달리 답할 것도 없는 호출은 거부된다. if필드를 도구 이벤트가 아닌 곳(예:Stop)에 붙이면 훅이 실행되지 않는다.Stop은 사용자 중단에는 실행되지 않고, API 오류로 끝난 턴은StopFailure가 받는다.
2. "hook error"가 보인다#
트랜스크립트에 "PreToolUse hook error: ..." 같은 메시지가 보이면, 흔한 원인은 스크립트가 예상 밖의 0 아닌 코드로 끝난 경우다. 원본은 아래 원인들도 든다(JSON 검증·파싱 오류는 종료 코드 0에서도 생긴다). 샘플 JSON을 파이프로 넣어 직접 시험한다:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $? # Check the exit code- "command not found": 절대 경로나
${CLAUDE_PROJECT_DIR}로 스크립트를 가리킨다. 셸 따옴표 문제를 아예 피하려면"args": []를 더해 셸 없이 스크립트를 직접 띄우는 exec form으로 바꾼다. - "jq: command not found":
jq를 설치하거나 Python·Node.js로 JSON을 파싱한다. blocking because Git Bash wasn't found: Windows에서 Claude Code가 Git Bash를 찾지 못했고, 실패하거나 시간 초과된 훅이 도구 호출을 막은 경우다. 해결책은 레퍼런스의 "Failed hooks on Windows without Git Bash"에 있다: Bash용 훅이면 Git for Windows를 설치하거나CLAUDE_CODE_GIT_BASH_PATH를 정하고 새 세션을 시작,.exe하나를 돌리는 훅이면 exec form(command+args)과"onFailure": "block", PowerShell용이면"shell": "powershell"과"onFailure": "block", 실패해도 진행돼야 하면"onFailure": "continue". 이 오류가 뜨는 훅은 0·2가 아닌 종료 코드를 모두 실패로 친다. Claude Code 훅 레퍼런스- JSON 검증(validation) 메시지: stdout이 JSON으로 파싱됐지만 스키마 검증에 실패했다. JSON 파싱(parse) 메시지: stdout이 JSON 객체처럼 보였지만 올바른 JSON이 아니다. 둘 다 종료 코드 0에서도 생긴다. 파싱 실패는 문자열 이어 붙이기 대신
jq같은 JSON 인코더로 페이로드를 만들어 고친다. - 스크립트가 아예 돌지 않으면 실행 권한을 준다:
chmod +x ./my-hook.sh
종료 코드가 0도 2도 아닌 오류는 대부분 막지 않는 오류라 동작은 진행된다. 트랜스크립트에는 <hook name> hook error 알림과 Failed with non-blocking status code: 뒤에 stderr 첫 줄만 보이므로, stderr 전체는 claude --debug나 세션 중 /debug로 디버그 로그를 켜서 본다. Claude Code 훅 가이드
3. /hooks에 훅이 없다#
- 파일 편집은 보통 자동 반영된다. 몇 초 뒤에도 안 보이면 파일 감시기가 놓쳤을 수 있으니 세션을 다시 시작한다.
- JSON이 올바른지 본다. 끝 쉼표와 주석은 허용되지 않는다.
- 파일 위치를 확인한다: 프로젝트 훅은
.claude/settings.json, 전역 훅은~/.claude/settings.json. - 메뉴에
Only hooks from managed settings run here가 보이면 조직이allowManagedHooksOnly를 켠 것이다. user·project·local 설정 파일의 훅은 실행되지 않고 목록에도 없다. → managed settings - 설정에
"disableAllHooks": true가 있는지도 본다. 우선순위를 적용한 뒤의 값을 읽으므로 프로젝트 설정이 사용자 설정을 덮을 수 있다. 편집자 해석: 원본은 이것을 문제 해결 절이 아니라 설정 위치 절에서 말하므로, 점검 항목으로 넣은 것은 편집자 판단이다. 근거는 원본의 "a project's settings file can override yours" 문장이다.
4. Stop 훅이 블록 상한에 걸린다#
Claude가 멈추지 않고 계속 일하다가, Stop 훅이 연속으로 너무 많이 막았다는 경고와 함께 턴을 끝낸다. Claude Code는 Claude의 도구 호출 없이 Stop 훅이 8번 연속 막으면 그 훅의 차단을 무시하고(override) 턴을 끝낸다. 스크립트가 이미 계속을 일으켰는지 확인하도록 입력의 stop_hook_active를 읽고 true면 일찍 끝낸다:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Allow Claude to stop
fi
# ... rest of your hook logic정말 8번보다 많이 돌아야 수렴하는 훅이면 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP 환경 변수로 상한을 올린다. Claude Code 훅 가이드
5. JSON을 냈는데 효과가 없다#
올바른 JSON을 출력했는데 결정이 적용되지 않고 트랜스크립트에 오류도 없다. 원인은 둘 중 하나다.
JSON 앞에 다른 출력이 있다. 훅은 비대화형 셸에서 돌지만 Git Bash나 BASH_ENV가 ~/.bashrc를 가리키는 설정 등에서는 프로필을 읽고, 프로필이 찍은 내용이 JSON보다 먼저 stdout에 나간다:
Shell ready on arm64
{"decision": "block", "reason": "Not allowed"}async 훅이 아니면 Claude Code는 {로 시작하지 않는 출력을 일반 텍스트로 읽어 JSON을 무시하고, 종료 코드가 0이므로 오류도 보이지 않는다. claude --debug로 시작해 훅을 일으키고 디버그 로그에서 Hook output does not start with {를 찾아 확인한다. 고치려면 프로필의 echo를 대화형 셸에서만 돌게 감싼다:
# In ~/.zshrc or ~/.bashrc
if [[ $- == *i* ]]; then
echo "Shell ready"
fi$-는 셸 플래그이고 i는 대화형이라는 뜻이다. 훅은 비대화형 셸에서 돌므로 이 echo를 건너뛴다.
필드가 잘못된 층에 있다. permissionDecision이나 additionalContext를 hookSpecificOutput 안이 아니라 최상위에 두면 JSON은 파싱되지만 Claude Code가 그 필드를 오류 없이 무시한다. claude --debug로 시작해 디버그 로그에서 Hook JSON output had unrecognized keys를 찾는다.
6. 훅이 무엇을 했는지 확인하기#
Ctrl+O로 트랜스크립트 보기를 열어 결과를 본다:
- 성공: 아무것도 보이지 않는다. 훅 JSON이
systemMessage나 Stop 훅 피드백처럼 무언가를 드러낼 때만 보인다. 실행 여부는 효과(다시 포맷된 파일 등)로 확인한다. - 막는 오류: 대부분의 이벤트에서는 막을 때 함께 온 메시지(예:
Blocked: rm commands are not allowed)가 보인다.ConfigChange·Elicitation같은 몇몇 이벤트에서는 메시지가 보이지 않는다. - 막지 않는 오류:
<hook name> hook error알림과 짧은 설명(stderr 첫 줄, 또는 JSON 검증·파싱 메시지)이 보이고, 동작은 진행됐다.
전체 실행 내역(종료 코드, stdout, stderr)은 디버그 로그에 있다. claude --debug-file /tmp/claude.log로 알려진 경로에 쓰고 다른 터미널에서 tail -f /tmp/claude.log로 본다. 플래그 없이 시작했으면 세션 중 /debug로 로그를 켜고 경로를 찾는다. 원본은 MCP 도구 훅 예에서 stdout을 JSON용으로 비워 두려고 메시지를 stderr에 쓰면 디버그 로그로 간다고도 말한다. Claude Code 훅 가이드
7. 레퍼런스가 더하는 점검 (2026-10-18 기준)#
막혀야 할 동작이 진행된다. Claude Code 훅 레퍼런스
- 종료 코드 1은 올바른 JSON이 없으면 막지 않는 오류다(실패하면 막도록 설정한 훅, 즉
onFailure: "block"은 예외). 막으려면 종료 코드 2를 쓴다. - 종료 코드 0에 출력이 없으면 승인이 아니라 "결정 없음"이다.
- 스크립트 경로가 틀리면 셸이 127 같은 코드로 끝나 막지 않는 오류가 되고, 훅은 사실상 한 번도 돌지 않는다. 알림의
Failed with non-blocking status code: /bin/sh: ...: No such file or directory를 찾는다. PreToolUse에서 시간 초과된 command·http·mcp_tool 훅은 호출을 막지 않는다.UserPromptSubmit은 시간 초과(기본 30초)면 출력을 버리고 프롬프트를 그대로 보낸다.- 실패해도 막으려면
"onFailure": "block"(v2.1.295 이상). 시험법과 한계는 Claude Code 차단 훅 단단하게 만들기. - 결정을 백그라운드 프로세스에서 내리면 늦게 찍힌 deny는 무시된다. 결정을 먼저 찍고 백그라운드 출력은
>/dev/null 2>&1로 돌린다.
matcher·if가 생각대로 안 맞는다. Claude Code 훅 레퍼런스
- matcher를 지원하지 않는 이벤트(
UserPromptSubmit,Stop,CwdChanged등)에 붙인matcher는 조용히 무시되고 매번 돈다. mcp__memory처럼.*없는 서버 matcher는 정확 문자열로 비교돼 아무 도구에도 맞지 않는다. 플러그인 서버는mcp__plugin_<plugin>_<server>__.*로 쓴다.- 특수 문자가 있으면 앵커 없는 정규식이라
Edit.*는NotebookEdit에도 맞는다. 전체 일치는^Edit$. - 플러그인 서브에이전트 이름(
my-plugin:reviewer)은 콜론 때문에 정규식으로 가므로^my-plugin:reviewer$로 쓴다. - Windows에서
Bash에만 맞춘 훅은 PowerShell 도구로 간 명령에 돌지 않는다.Bash|PowerShell로 맞춘다. 경로는 역슬래시로 오므로 비교 전에 정규화한다. FileChanged의 matcher는 문자 그대로의 파일 이름 목록이다.^\.env같은 정규식은 그런 이름의 파일을 감시할 뿐이다.
훅이 느리거나 무엇을 했는지 모르겠다. Claude Code 훅 레퍼런스
claude --debug의 로그 위치는~/.claude/debug/<session-id>.txt이고,--debug는 터미널에 출력하지 않는다.- v2.1.296 이상에서는 도구 이벤트·
UserPromptSubmit·SessionStart·Stop등의 command 훅이 끝날 때마다Hooks: PostToolUse:Write [.claude/hooks/log-write.sh] finished with status 0 (31ms)같은 줄을 남긴다. 끝난 방식은timed out after <N>ms,cancelled,moved to the background,failed to start일 수도 있다.Notification·SessionEnd·PreCompact등에서는 소요 시간 없는completed with status줄이다. - matcher 개수 같은 더 자세한 줄은
CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose. - stderr 전체는 디버그 로그에 있다(트랜스크립트 알림은 첫 줄만).
그 밖의 함정. Claude Code 훅 레퍼런스
Stop훅이transcript_path를 읽었는데 방금 응답이 없다: 트랜스크립트는 비동기로 쓰이니last_assistant_message를 쓴다.- async 훅 결과가 안 온다: 결과는 다음 대화 턴에 Claude에게 가고 사용자에게는 보이지 않는다. 세션이 쉬면 다음 입력까지 기다린다. 완료 알림은
Ctrl+O·--verbose에서만 보인다.-p에서는 종료 때cancelled로 끝난다. - 훅에서
/dev/tty에 쓰는 알림이 안 된다: 훅은 제어 터미널 없이 돈다.terminalSequence를 쓴다. → Claude Code 훅 설정 레시피 SessionStart의mcp_tool훅이 시작 때 안 돈다: 시작 시점에는 MCP 서버를 쓸 수 없어 건너뛴다(디버그 로그에no MCP client context). 시작 때 필요한 일은 command 훅으로 한다.FileChanged훅이 끝없이 돈다: 훅이 파일을 다시 쓰면 그 변경에 다시 돈다. 실제로 바꿀 것이 있을 때만 쓰도록 조건을 좁힌다.suppressOutput을 켰는데 달라진 게 없다: 이 필드는 효과가 없다. 성공한 훅의 stdout은 원래 트랜스크립트에 보이지 않는다.
관련#
- 훅 개념과 다른 기능의 경계: 훅
- 비슷한 증상별 점검 페이지: 스킬 문제 해결, CLAUDE.md 문제 해결
- 조직 정책이 훅을 막을 때: Claude Code managed settings 배포·점검
- 이벤트별 동작 찾아보기: 훅 이벤트 레퍼런스, 입출력 규칙: 훅 입출력 규칙