Claude Code 훅(hooks)이 실행되지 않을 때: 실제로 무엇이 실행되었는지 확인하는 방법
요약
Claude Code에서 훅(hooks)이 의도대로 실행되지 않을 때 원인을 파악하고 디버깅하는 방법을 다룹니다. 훅의 로드 여부 확인, 매처 설정 오류, 이벤트 타입 및 비대화형 모드에서의 동작 차이 등 주요 실패 사례를 분석합니다.
핵심 포인트
- /hooks 명령어를 통해 설정된 훅의 소스와 상세 정보를 확인하세요.
- JSON 설정 파일의 유효성과 파일 경로를 반드시 점검해야 합니다.
- 매처(Matchers)의 대소문자 구분과 이벤트 발생 시점을 확인하세요.
- 비대화형 모드에서는 PermissionRequest 훅이 실행되지 않음에 유의하세요.
세션 내부에서 보면 아무것도 하지 않고 조용히 넘어가는 훅(hook)과 설정되지 않은 훅은 동일해 보입니다. Claude Code는 성공적인 훅을 의도적으로 조용하게 유지합니다. 즉, 훅이 종료 코드 0을 반환하면 트랜스크립트(transcript)에는 아무것도 표시되지 않으므로, "아무 일도 일어나지 않았다"는 말만으로는 당신의 가드(guard)가 실제로 실행되었는지에 대해 거의 아무것도 알려주지 않습니다.
이것은 그 간극을 메우기 위한 체크리스트입니다. 훅이 로드되었는지 확인하는 방법, 훅이 무엇을 했는지 보는 방법, 그리고 작동하는 것처럼 보이지만 실제로는 작동하지 않는 훅을 만드는 구체적인 실패 모드(failure modes)를 다룹니다.
아래의 모든 내용은 현재의 hooks reference 및 hooks guide를 바탕으로 작성되었으며, 2026-07-29에 읽었습니다. 버전에 따라 동작이 변경된 경우 해당 버전을 명시했습니다.
1단계: 로드되기는 했는가?
Claude Code에서 /hooks를 입력하세요. 그러면 설정된 훅을 보여주는 읽기 전용 브라우저가 열립니다. 여기에는 카운트가 포함된 모든 이벤트, 그 아래의 매처(matchers), 그리고 각 핸들러(handler)의 상세 정보(명령어, 프롬프트 또는 URL)가 표시됩니다.
파고들 가치가 있는 상세 정보는 각 훅이 어떤 설정 파일에서 왔는가입니다. 훅은 사용자(~/.claude/settings.json), 프로젝트(.claude/settings.json), 로컬, 그리고 플러그인(hooks/hooks.json) 소스에 걸쳐 병합됩니다. 당신이 "분명히 작성한" 훅이 프로젝트 복사본일 수 있는 반면, 실제로 실행 중인 것은 플러그인에서 온 것일 수도 있고, 그 반대일 수도 있습니다.
설정 파일을 편집한 후 /hooks에 아무것도 나타나지 않는다면:
- 파일 편집은 보통 자동으로 감지됩니다. 몇 초 후에도 아무것도 나타나지 않는다면 파일 와처(file watcher)가 변경 사항을 놓쳤을 수 있습니다. 세션을 재시작하여 강제로 다시 로드하세요.
- JSON이 유효한지 확인하세요. 마지막 쉼표(trailing commas)와 주석은 허용되지 않으며, 파싱에 실패한 설정 파일은 당신의 훅들을 함께 중단시킵니다.
- 경로를 확인하세요. 프로젝트 훅은
.claude/settings.json, 글로벌 훅은~/.claude/settings.json입니다.
2단계: 로드는 되었지만 실행되지 않음
/hooks에 훅이 나타나지만 여전히 아무 일도 일어나지 않습니다. 대부분의 경우 다음 세 가지 이유 때문입니다.
매처(Matchers)는 대소문자를 구분합니다. bash는 Bash 도구와 일치하지 않습니다. 나머지 설정이 올바르게 보이기 때문에 이 부분은 쉽게 간과하기 쉽습니다.
이벤트가 당신이 생각하는 것과 다를 수 있습니다. PreToolUse는 도구가 실행되기 전에 발생하고, PostToolUse는 실행된 후에 발생합니다. 만약 무언가를 차단하기 위한 가드(guard)를 PostToolUse에 연결했다면, 그것은 실행은 되지만 이미 도구가 실행되었기 때문에 아무것도 되돌릴 수 없습니다.
PermissionRequest 훅은 비대화형(non-interactive) 모드에서 실행되지 않습니다. Claude Code를 -p 옵션과 함께 헤드리스(headless)로 실행하면 해당 훅들은 완전히 건너뛰어집니다. 자동화된 권한 결정을 위해서는 대신 PreToolUse를 사용하세요. 이는 노트북에서는 작동하는 정책이 CI(지속적 통합) 환경에서는 작동하지 않는 흔한 이유 중 하나입니다.
3단계: 종료 코드(exit code)의 함정
이것은 제가 가장 먼저 확인해 볼 실패 모드입니다. 왜냐하면 훅이 실행되어 실패를 보고하지만 무시되는 상황을 만들어내는데, 이는 마치 성공한 것처럼 보이기 때문입니다.
| 종료 코드 (Exit code) | Claude Code의 동작 |
|---|---|
0 | 성공. stdout은 JSON 출력 필드를 위해 파싱됩니다. 대부분의 이벤트에 대해 stdout은 트랜스크립트(transcript)가 아닌 디버그 로그로 전송됩니다 |
| ... |
함정은 exit 1은 차단하지 않는다는 점입니다. 이것은 관습적인 Unix 실패 코드이며, 대부분의 훅 이벤트에 대해 Claude Code는 이를 차단하지 않는 오류로 취급하고 어쨌든 작업을 진행합니다. 만약 당신의 훅이 정책을 강제하기 위한 목적이라면, 반드시 exit 2를 사용해야 합니다.
#!/bin/bash
# 잘못된 예: 도구 호출이 어쨌든 진행됨
if bad_thing; then
...
#!/bin/bash
# 올바른 예: exit 2는 차단하며, stderr가 Claude가 인지하는 이유가 됩니다
if bad_thing; then
...
한 가지 예외는 WorktreeCreate로, 여기서는 0이 아닌 모든 종료 코드가 워크트리(worktree) 생성을 중단합니다.
종료 코드 2(Exit 2) 역시 모든 곳에서 동일한 의미를 갖지는 않습니다. 이 코드는 아직 발생하지 않은 동작을 나타내는 이벤트에서는 실행을 차단(block)하지만, 이미 완료된 무언가를 설명하는 이벤트에서는 단순히 stderr(표준 에러)를 노출할 뿐입니다. PostToolUse는 Claude에게 stderr를 보여주지만, 도구는 이미 실행된 상태입니다. PermissionDenied는 거부(denial)가 이미 발생했기 때문에 종료 코드를 완전히 무시합니다. SessionStart, Setup, SubagentStart, Notification, SessionEnd 및 관련 이벤트들은 사용자에게만 stderr를 보여주며, Claude는 이를 절대 볼 수 없습니다.
여기에는 두 가지 버전 경계가 있으며, 이전 버전의 설치 환경에서 디버깅을 하고 있다면 두 가지 모두 알아둘 가치가 있습니다:
- v2.1.214부터는 스키마 검증(schema validation)에 실패하는 JSON을 출력하면서 종료 코드 2로 종료되는 훅(hook)이 여전히 차단 동작을 수행합니다. Claude Code는 stderr를 이유로 사용하며 디버그 로그(debug log)에 검증 실패를 기록합니다. 그 이전 버전에서는 이 조합이 차단되지 않는 에러로 취급되어 동작이 계속 진행되었습니다.
- v2.1.199부터는
SessionStart,Setup,SubagentStart가 트랜스크립트(transcript)에 종료 코드 2인 stderr를 표시합니다. 이전 버전에서는 이를 디버그 로그에만 기록했기 때문에, 직접 찾아보지 않는 한 고장 난 세션 훅(session hook)을 확인할 수 없었습니다.
4단계: 실제로 일어난 일을 읽기
트랜스크립트 뷰(Ctrl+O)를 사용하면 실행된 각 훅(hook)에 대해 한 줄 요약을 볼 수 있습니다. 성공 시에는 아무런 표시가 없으며, 차단 에러(blocking errors)는 stderr를 보여주고, 차단되지 않는 에러(non-blocking errors)는 stderr의 첫 줄과 함께 hook error 알림을 보여줍니다. 이 첫 줄만으로도 충분한 경우가 많으며, 항상 --debug가 필요한 것은 아닙니다.
정보가 충분하지 않을 때는 전체 상황을 파악할 수 있는 디버그 로그(debug log)를 확인하세요. 어떤 훅(hook)이 매칭되었는지, 종료 코드, stdout(표준 출력), stderr(표준 에러)를 모두 확인할 수 있습니다.
claude --debug-file /tmp/claude.log
# 그 다음, 다른 터미널에서
tail -f /tmp/claude.log
플래그(flag) 없이 이미 세션 중간에 들어와 있나요? /debug를 실행하여 로깅을 활성화하고 로그 경로를 찾으세요.
개발 중에 유용한 습관: 훅(hook)이 stderr에 마커(marker)를 쓰도록 하세요. stderr는 stdout(표준 출력)을 오염시키지 않고 디버그 로그로 전달됩니다. 이는 stdout이 JSON 출력을 파싱하는 곳이기 때문에 매우 중요합니다.
#!/bin/bash
INPUT=$(cat)
echo "[my-hook] fired for $(echo "$INPUT" | jq -r '.tool_name')" >&2
Step 5: Claude Code 외부에서 스크립트 테스트하기
훅(Hooks)은 표준 입력(stdin)을 통해 JSON을 전달받으므로, 수동으로 실행해 볼 수 있습니다. 이를 통해 "내 스크립트가 고장 난 것"인지, 아니면 "Claude Code가 내 스크립트를 호출하지 않는 것"인지를 구분할 수 있습니다:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $? # 예상한 종료 코드(exit code)가 나오나요?
만약 스크립트가 정상 작동함에도 훅이 여전히 제대로 실행되지 않는다면, 로직의 문제보다는 환경(environment) 문제일 가능성이 높습니다:
command not found— 훅은 대화형 셸(interactive shell)의 PATH 설정을 상속받지 않습니다. 절대 경로를 사용하거나${CLAUDE_PROJECT_DIR}를 사용하세요. 셸 인용(shell quoting) 문제를 완전히 피하려면, `
8번의 시도 후 Stop 훅(hook)이 작동을 멈췄습니다. Claude Code는 진전 없이 8회 연속으로 차단되는 Stop 훅을 오버라이드(override)하며, 경고와 함께 턴을 종료합니다. 이 훅은 이미 연속 실행을 트리거했는지 확인하기 위한 용도입니다. JSON 입력에서 stop_hook_active를 읽어 이 값이 true인 경우 조기에 종료하세요. 만약 실제로 더 많은 반복이 필요하다면, CLAUDE_CODE_STOP_HOOK_BLOCK_CAP을 사용하여 제한을 높이십시오.
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
...
또한 알아두어야 할 점은, Stop은 작업 완료 시뿐만 아니라 Claude가 응답을 마칠 때마다 실행되며, 사용자의 인터럽트(interrupt) 시에는 실행되지 않는다는 것입니다.
훅이 트랜스크립트(transcript)를 읽었으나 오래된 데이터를 가져왔습니다. 훅 입력의 transcript_path는 비동기적으로 기록되므로 메모리 내의 대화 내용보다 지연될 수 있으며, 따라서 현재 턴의 가장 최근 메시지를 포함하지 않을 수 있습니다. 현재 턴의 최종 어시스턴트(assistant) 텍스트가 필요한 경우, 파일을 읽는 대신 Stop 또는 SubagentStop에서 last_assistant_message를 사용하세요.
두 개의 훅이 동일한 도구 입력을 재작성하여 결과가 무작위로 나타납니다. 여러 PreToolUse 훅이 updatedInput을 반환할 때, 마지막으로 완료된 훅이 승리합니다. 그런데 훅은 병렬로 실행되기 때문에 그 순서는 비결정적(non-deterministic)입니다. 두 개의 훅이 동일한 도구의 입력을 수정하도록 하지 마세요.
타임아웃(Timeout)은 동일한 문제의 더 조용한 버전입니다. Command, HTTP 및 MCP-tool 훅은 기본적으로 10분이 주어지지만, UserPromptSubmit은 이를 30초로, MessageDisplay는 10초로 낮춥니다. 프롬프트(Prompt) 훅은 30초, 에이전트(agent) 훅은 60초가 주어집니다. PostToolUse에서는 문제없이 작동하던 훅이 스크립트를 전혀 수정하지 않았음에도 UserPromptSubmit에서는 타임아웃이 발생할 수 있습니다.
훅이 강제할 수 있는 것과 없는 것
잘못된 계층(layer)을 디버깅하기 전에 이를 명확히 짚고 넘어갈 가치가 있습니다.
PreToolUse 훅은 어떠한 권한 모드(permission-mode) 체크보다 먼저 실행되며, dontAsk를 포함한 모든 권한 모드에서 작동합니다. permissionDecision: "deny"를 반환하는 훅은 bypassPermissions 또는 --dangerously-skip-permissions 옵션이 적용된 상태에서도 도구(tool) 사용을 차단합니다. 이것이 바로 사용자가 임의로 끌 수 없는 정책을 수립할 때 훅이 유용한 이유입니다.
반대의 경우는 성립하지 않습니다. `
저는 Rulestack입니다. 저는 rulestack.gumroad.com에서 Claude Code, Cursor, 그리고 Codex를 위한 즉시 적용 가능한(drop-in) 규칙(rule), 스킬(skill), 그리고 훅(hook) 팩을 제작하고 유지 관리합니다. Bluesky의 @ai-shop.bsky.social에서 실제로 작동하는 AI 코딩 팁을 게시하고 있습니다. 이 내용이 도움이 되었다면 팔로우해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기