이 페이지의 목차
확인 2026-10-18근거 자료 2건갱신 2026-10-26언급 11곳
이 페이지의 목차
확인 날짜 안내

공식 훅 가이드 Claude Code 훅 가이드(원본 날짜 미상, 수집 2026-10-18)의 문제 해결 절을 증상별로 정리했다. 7절은 공식 Hooks reference Claude Code 훅 레퍼런스(원본 날짜 미상, 수집 2026-10-18)가 더하는 점검이다. 훅을 처음 만드는 법은 Claude Code 훅 설정 레시피.

증상별 점검표#

증상먼저 볼 것
설정했는데 한 번도 안 돈다/hooks에 맞는 이벤트 아래 있는지, matcher(용어 사전) 대소문자, 이벤트 종류(PreToolUse는 실행 전, PostToolUse는 실행 후)
트랜스크립트에 "PreToolUse hook error: ..."스크립트를 샘플 JSON으로 직접 돌려 종료 코드 확인, 경로·jq·실행 권한
/hooks에 아무 훅도 없다JSON 문법(끝 쉼표·주석 불가), 파일 위치, 세션 재시작, allowManagedHooksOnly
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가 받는다.

근거: Claude Code 훅 가이드

2. "hook error"가 보인다#

트랜스크립트에 "PreToolUse hook error: ..." 같은 메시지가 보이면, 흔한 원인은 스크립트가 예상 밖의 0 아닌 코드로 끝난 경우다. 원본은 아래 원인들도 든다(JSON 검증·파싱 오류는 종료 코드 0에서도 생긴다). 샘플 JSON을 파이프로 넣어 직접 시험한다:

bash
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" 문장이다.

근거: Claude Code 훅 가이드

4. Stop 훅이 블록 상한에 걸린다#

Claude가 멈추지 않고 계속 일하다가, Stop 훅이 연속으로 너무 많이 막았다는 경고와 함께 턴을 끝낸다. Claude Code는 Claude의 도구 호출 없이 Stop 훅이 8번 연속 막으면 그 훅의 차단을 무시하고(override) 턴을 끝낸다. 스크립트가 이미 계속을 일으켰는지 확인하도록 입력의 stop_hook_active를 읽고 true면 일찍 끝낸다:

bash
#!/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를 대화형 셸에서만 돌게 감싼다:

bash
# 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를 찾는다.

근거: Claude Code 훅 가이드

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은 원래 트랜스크립트에 보이지 않는다.

관련#