스크랩

Claude Code 훅, exit 1로는 못 막는다: exit 2만 '차단'으로 인정되는 이유와 셸 파이프 함정

Claude Code 훅, exit 1로는 못 막는다: exit 2만 '차단'으로 인정되는 이유와 셸 파이프 함정

한눈에 보는 핵심 요약

Claude Code 훅으로 테스트 게이트를 만들 때 종료 코드 1과 2는 결과가 완전히 다릅니다.

  • exit 1: 검사기가 "막음"을 출력하고 끝나도 Claude Code는 도구 실행을 막지 않습니다.
  • exit 2: 공식 문서 표현대로 "Exit 2 means a blocking error." 이 코드로 끝나야 실제로 차단됩니다.
  • 일반 셸 스크립트는 반대로 0이 아닌 코드를 모두 실패로 처리합니다. 다만 | tee처럼 파이프를 쓰면 검사기의 실패 코드가 사라질 수 있습니다.

같은 검사기라도 어디서 실행되느냐에 따라 "실패"의 뜻이 달라집니다. 그래서 훅을 만들 때 이 차이를 알고 있어야 합니다.


실험: exit 1로 끝나는 훅은 Write를 막았을까?

원문 작성자는 PreToolUse(Write) 훅에 검사 스크립트를 걸고 아래 명령을 실행했습니다.

claude -p --model haiku --setting-sources project --allowedTools "Write" \
  "hello.txt 파일을 만들고 hi 한 줄을 써. 막히면 막힌 이유를 한 줄로 적고 다른 방법은 쓰지 마."

훅이 exit 1로 끝난 경우

완료했습니다. hello.txt 파일이 생성되었고 "hi"가 한 줄 들어있습니다.
hello.txt: 생김(hi)

훅이 "막음"이라고 출력했는데도 모델은 작업을 끝냈다고 답했고 파일도 만들어졌습니다. 게이트가 아무것도 막지 못한 것입니다.

훅이 exit 2로 끝난 경우

같은 스크립트에서 마지막 종료 코드만 2로 바꾸자 Write가 차단됐습니다. 공식 훅 문서에 적힌 동작과 같습니다.

💡 정리하면 Claude Code 훅에서 차단 신호는 exit 2 하나뿐입니다. exit 1을 포함한 다른 0이 아닌 코드는 오류로 기록될 수는 있어도 도구 실행을 멈추지는 않습니다.


셸에서 생기는 또 다른 함정: 파이프와 tee

셸 스크립트나 CI에서는 0이 아닌 종료 코드가 모두 실패로 처리됩니다. 여기까지는 예상대로입니다. 문제는 검사 결과를 파일로 남기려고 tee를 붙였을 때 생겼습니다.

./check.sh | tee gate.log && ./upload.sh

파이프라인의 종료 코드는 기본적으로 마지막 명령의 종료 코드입니다. 그래서

  1. check.sh가 1(막음)로 끝나도
  2. tee가 0으로 끝나면
  3. 파이프라인 전체가 성공으로 처리되고
  4. 실행되면 안 되는 올리기 단계가 실행됐습니다.

해결: set -o pipefail

set -o pipefail
./check.sh | tee gate.log && ./upload.sh

pipefail을 켜면 파이프라인 중 하나라도 실패했을 때 전체가 실패로 처리됩니다. 로그를 tee로 남기는 게이트 스크립트라면 이 옵션을 켜 두는 편이 안전합니다.


작성자가 정한 규칙

원문 작성자는 실험 결과를 바탕으로 아래처럼 규칙을 고정했습니다.

1. 검사기의 종료 코드 의미를 하나로 고정

| 종료 코드 | 의미 | |---|---| | 0 | 통과 | | 1 | 막음 | | 2 | 판정 불가 |

2. 실행 환경별로 맞추기

  • 셸 / CI: set -o pipefail을 켜서 파이프 끝의 tee가 실패 코드를 덮지 못하게 합니다.
  • Claude Code 훅: 막아야 하는 경우(검사기의 1과 2)는 모두 exit 2로 바꿔서 내보냅니다.

훅 래퍼는 예를 들어 이렇게 쓸 수 있습니다.

#!/usr/bin/env bash
set -o pipefail
./check.sh 2>&1 | tee -a gate.log
rc=$?
if [ "$rc" -ne 0 ]; then
  echo "테스트 게이트 실패 (code=$rc)" >&2
  exit 2   # Claude Code는 exit 2만 차단으로 처리함
fi
exit 0

검사기 쪽 의미(통과/막음/판정 불가)는 그대로 두고, 어떻게 막을지는 실행 환경에 맞게 래퍼에서 처리하는 방식입니다.


알아둘 한계

작성자가 밝힌 재현 범위는 다음과 같습니다.

  • 버전: Claude Code 2.1.281
  • 모델: haiku
  • 훅 이벤트: PreToolUse(Write) 한 가지
  • 반복 횟수: 경우별 1회

다른 훅 이벤트(PostToolUse, Stop, UserPromptSubmit 등)에서 exit 1과 2가 어떻게 동작하는지는 확인하지 않았습니다. 다른 이벤트에 게이트를 걸 계획이라면 직접 재현해 보고 공식 훅 문서에서 해당 이벤트의 종료 코드 동작도 확인하시길 권합니다.


마무리

AI 코딩 에이전트에 "테스트가 실패하면 진행하지 마"라는 게이트를 걸 때는 그 게이트가 실제로 막는지 확인해야 합니다. 검사기가 실패를 출력했다고 해서 차단까지 된다는 보장은 없습니다.

  • Claude Code 훅에서 차단하려면 exit 2를 쓰세요.
  • 셸 파이프라인에는 **set -o pipefail**을 켜세요.
  • 검사기의 종료 코드 의미는 한 가지로 고정하고, 환경별 변환은 래퍼에서 처리하세요.

게이트를 만들었다면 일부러 실패하는 경우를 한 번 실행해서 정말 막히는지 직접 확인해 보는 것을 추천합니다.


원문: https://news.hada.io/topic?id=36873&utm_source=geeknews&utm_medium=rss&utm_campaign=news