Desk Clarity

Everyday document & browser help

Claude Code 훅이 막지 못하거나 멈출 때: PreToolUse 종료 코드와 출력 확인

By ON Lab · Sources checked November 7, 2026

훅 스크립트가 실패했다고 Claude Code의 도구 호출까지 차단됐다고 판단하면 안 됩니다. 먼저 이벤트가 PreToolUse인지 확인하고, 프로세스 종료 코드·표준 출력·표준 오류를 따로 읽으세요. 현재 공식 문서에서 PreToolUse의 exit 2는 도구 호출을 차단합니다. 일반적인 exit 1 오류와 같은 의미가 아닙니다.

반대 증상도 있습니다. 훅이 호출을 막은 뒤 모델이 더 이상 진행하지 않거나, 수정 사유가 제대로 전달되지 않았다고 보고하는 사례입니다. 훅의 실행 결과, Claude가 받은 설명, 이후 모델의 행동은 서로 다른 관측 항목입니다. 하나가 확인됐다고 나머지까지 성공으로 처리하지 마세요.

같은 실패 메시지라도 먼저 이벤트를 구분하기

이벤트현재 문서의 exit 2 동작검수할 결과
PreToolUse도구 호출 차단도구가 실제 실행되지 않았는지
PostToolUse이미 실행된 도구 뒤에 stderr 전달실행 결과와 후속 설명을 별도로 확인
Stop정지를 막고 대화 계속종료 훅 반복 여부와 입력 상태
PermissionRequestexit 2만으로 권한 흐름을 차단하지 않음해당 이벤트의 결정 객체 문서 확인

이 표는 2026년 11월 7일 열람한 공식 훅 참조를 요약한 것입니다. 특히 PermissionRequest에 PreToolUse 예제를 그대로 붙이면 의도한 결과를 얻지 못할 수 있습니다. 문서가 계속 바뀌는 도구이므로 사용 중인 버전과 이벤트 이름을 기록하고 현재 원문도 대조하세요.

exit 1을 정책 거부로 사용하지 않기

현재 문서는 종료 코드가 0도 2도 아니고 stdout이 일반 텍스트이거나 비어 있는 경우, 별도로 실패 시 차단하도록 정의된 훅이 아니라면 비차단 오류로 설명합니다. 스크립트 경로가 틀려 셸이 127을 반환하는 경우도 훅의 정책 검사가 수행된 것으로 볼 수 없습니다.

따라서 실패 코드를 발견하면 “잘 막혔다”가 아니라 “어떤 검사까지 실행됐는가”부터 확인해야 합니다. JSON 파싱 실패, 인터프리터 없음, 파일 접근 실패와 의도한 정책 거부를 구분하세요. 중요한 정책은 문자열 패턴 훅 하나를 완전한 보안 경계로 취급하지 말고 공식 권한 체계와 함께 설계해야 합니다.

안전한 로컬 예제로 세 채널을 분리하기

아래 코드는 실제 명령을 실행하지 않습니다. 입력의 command가 시험용 값과 정확히 같을 때만 차단 사유를 stderr에 출력하고 2로 종료하는 Python 스크립트입니다. 실제 Claude Code 설정에 등록하기 전에 독립적으로 입출력을 확인하는 예제이며, 운영 정책으로 바로 복사할 차단 목록은 아닙니다.

import json
import sys

payload = json.load(sys.stdin)
command = payload.get("tool_input", {}).get("command", "")
if command == "demo-block":
    print("Demo blocked; choose the documented alternative.", file=sys.stderr)
    raise SystemExit(2)
raise SystemExit(0)

파일 이름을 hook_check.py로 저장한 후 {"tool_input":{"command":"demo-block"}}를 stdin으로 전달하면 종료 코드 2, 빈 stdout, 설명이 있는 stderr가 예상 결과입니다. command를 demo-pass로 바꾸면 종료 코드 0이며 두 출력이 비어 있어야 합니다. 사용자 명령을 eval이나 subprocess로 실행하는 예제가 아닙니다.

이 글의 검사에서는 두 입력을 실제 Python 프로세스에 전달해 종료 코드와 출력 전체를 대조했습니다. Claude Code의 인증 세션에서 훅을 등록하거나 실제 도구를 차단한 시험은 수행하지 않았습니다. 로컬 스크립트 검사는 프로그램의 분기만 확인하며, Claude Code의 이벤트 연결·권한 적용·모델 후속 행동을 검증하지 않습니다.

JSON을 쓸 때는 오래된 설명을 그대로 적용하지 않기

현재 공식 문서는 PreToolUse의 exit 2에 유효한 JSON이 있으면 출력 필드를 읽을 수 있지만 차단 결정을 해제할 수는 없다고 설명합니다. JSON에 allow가 있어도 exit 2의 차단을 뒤집지 못합니다. 차단 결정을 담은 JSON의 사유가 있으면 stderr보다 그 사유를 사용하는 경우도 있습니다. 따라서 “exit 2이면 모든 JSON을 언제나 무시한다”는 문장은 현재 PreToolUse 전체를 설명하지 못합니다.

구조화된 결정 출력을 의도한다면 exit 0과 해당 이벤트의 스키마를 함께 확인하세요. stdout에는 주위 설명 없이 하나의 JSON 객체만 두고, 진단 로그는 별도로 분리합니다. JSON 문법이 맞는 것과 이벤트 스키마를 통과하는 것은 다릅니다. 단순히 json 파서에서 통과했다는 이유로 Claude Code가 결정을 적용했다고 기록하지 마세요.

차단 후 대화까지 멈추면 다음을 기록하기

연결한 GitHub 독자 이슈에는 PreToolUse exit 2 이후 모델 진행이 멈췄다는 초기 보고와 여러 후속 설명이 있습니다. 작성자의 마지막 정리는 병렬 훅 충돌 가능성과 단일 dispatcher 우회책을 언급합니다. 이것은 특정 사용자의 사례이며 모든 환경의 확정 원인이나 공식 보장 수정이 아닙니다.

  1. Claude Code 버전, 운영체제, 이벤트, matcher를 기록합니다.
  2. 원래 요청의 도구와 민감정보를 지운 입력 형태를 보존합니다.
  3. 각 훅의 종료 코드, stdout, stderr를 분리해 읽습니다.
  4. 동일 matcher에 여러 훅이 있는지 확인합니다. 현재 공식 문서는 일치하는 훅이 병렬 실행될 수 있다고 설명합니다.
  5. 허용된 별도 시험 프로젝트에서 한 훅씩 비교합니다. 조직의 필수 훅을 임의로 끄거나 권한 모드를 우회하지 않습니다.

전체 stderr와 실행 정보를 확인할 때 공식 문서의 claude --debug-file <path> 같은 진단 방법을 사용하되 로그에 파일 경로나 명령 인수가 포함될 수 있으므로 공개 전 비밀정보를 제거하세요. 이 글은 해당 Claude 명령의 실제 인증 실행을 재현하지 않았습니다.

검수 완료 기준

스크립트 단위 검사는 예상 종료 코드와 출력이 모두 맞을 때 완료입니다. Claude Code 연결 검사는 의도한 이벤트가 호출되고 실제 도구 결과가 정책과 맞는지까지 확인해야 합니다. 차단 사유를 모델이 읽고 적절히 수정했는지는 별도의 결과입니다. 세 단계 중 확인하지 않은 단계는 미확인으로 남기세요.

허용 규칙의 범위 문제라면 허용했는데도 다시 묻는 권한 확인표를 함께 보세요. 권한 질문과 훅 차단을 분리해야 같은 설정을 반복해서 넓히는 일을 피할 수 있습니다.

검증 범위: 현재 공식 훅 문서와 독자 이슈 전체를 대조하고 원본 Python 예제의 두 분기를 실행했습니다. Claude Code 런타임·사용자 저장소·조직 정책은 시험하지 않았습니다.

Sources