본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 10. 12:04

Claude Code hook exit code 완전 가이드 — 0, 1, 2를 올바르게 구분하여 사용하기

요약

Claude Code의 hooks에서 사용하는 exit code(0, 1, 2)의 정확한 의미와 차이점을 설명합니다. 특히 exit 1은 경고일 뿐 차단 기능이 없으므로, 명령을 중단하려면 반드시 exit 2를 사용해야 함을 강조합니다.

핵심 포인트

  • exit 0: 허가 또는 판정 유보 (자동 승인 포함)
  • exit 1: 에러/경고 발생 (사용자에게 표시되나 작업은 계속 진행)
  • exit 2: 조작 차단 (작업 중단 및 모델에게 에러 메시지 전달)
  • 차단 목적의 가드 스크립트 작성 시 반드시 exit 2를 사용해야 함

Claude Code의 hooks에서 안전 가드(safety guard)를 작성했다. rm -rf /를 차단하는 것이다. 테스트했다. 작동했다. 안심했다.

일주일 후, 로그를 보고 얼굴이 창백해졌다. 차단했어야 할 명령어가 전부 통과되고 있었다.

원인은 단 한 글자였다. exit 1이라고 써야 할 곳을…… 아니, exit 2라고 써야 할 곳을 exit 1로 작성했다.

hook의 exit code는 세 가지뿐이다. 0, 1, 2. 하지만 1과 2의 의미가 직관과는 정반대다. 이 기사에서 완전히 이해하도록 하자.

exit code의미조작에 미치는 영향stderr의 행방
0허가 (또는 의견 없음)그대로 계속 진행표시되지 않음
1에러/경고그대로 계속 진행사용자에게 표시
2차단 (Block)조작을 중지모델에게 전달됨

최중요 포인트: exit 1은 차단이 아니다. 계속 진행된다. 차단할 수 있는 것은 exit 2뿐이다.

hook을 통과시킨다. 자동 승인(auto-approve) hook은 이것을 반환한다.

#!/bin/bash
# auto-approve-readonly.sh
# TRIGGER: PreToolUse MATCHER: "Bash"
...

두 가지 포인트가 있다.

  • 자동 승인의 exit 0: echo '{"decision":"approve",...}'를 stdout에 출력하고 exit 0을 한다. 이렇게 하면 사용자에 대한 확인 프롬프트가 스킵된다.
  • '의견 없음'의 exit 0: 마지막의 exit 0은 "이 hook에서는 판정하지 않음"을 의미한다. 다른 hook이나 기본 동작에 판단을 맡긴다.

둘 다 exit 0이지만, stdout에 JSON을 출력하느냐에 따라 동작이 달라진다.

stderr에 작성한 내용이 사용자에게 표시된다. 하지만 조작은 멈추지 않는다.

#!/bin/bash
# disk-space-check.sh
# TRIGGER: Notification MATCHER: ""
...

exit 1의 용도는 한정적이다. "알리고 싶지만, 멈추고 싶지는 않은" 상황에서만 사용한다.

  • 디스크 용량이 적음 → 경고만 내보내고 계속 진행
  • 권장되지 않는 패턴을 검출함 → 주의 환기만 하고 계속 진행
  • 세션 시간이 김 → 리마인드만 하고 계속 진행

조작을 완전히 중지한다. stderr의 내용은 모델에게 전달되며, 모델은 그것을 읽고 행동을 바꾼다.

#!/bin/bash
# destructive-guard.sh
# TRIGGER: PreToolUse MATCHER: "Bash"
...
#!/bin/bash
# npm-publish-guard.sh
# TRIGGER: PreToolUse MATCHER: "Bash"
...
#!/bin/bash
# aws-production-guard.sh
# TRIGGER: PreToolUse MATCHER: "Bash"
...

exit 2로 차단하면, 모델은 stderr 메시지를 읽는다. 따라서 "왜 차단했는지"를 적어두면, 모델이 스스로 다른 방법을 생각한다. "BLOCKED: npm publish requires manual confirmation."라고 적으면, 모델은 --dry-run을 붙여서 재시도할지도 모른다.

이것이 가장 위험한 버그다.

# NG: 차단했다고 생각했지만 그냥 통과됨
if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
echo "BLOCKED: dangerous command" >&2
...

exit 1은 많은 셸 스크립트(shell script)에서 "실패"를 의미한다. 그래서 차단의 의미라고 착각하기 쉽다. 하지만 Claude Code의 hooks에서는 **exit 1 = 경고 (계속 진행), exit 2 = 차단 (중지)**이다.

올바르게는 이렇게 작성한다.

# OK: 확실하게 차단됨
if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
echo "BLOCKED: dangerous command" >&2
...

암기법: "2로 멈춘다". 한 글자 차이가 보안 홀(security hole)이 된다.

hook을 작성했다면, 반드시 exit code를 확인하라.

# 테스트용 JSON을 주입하여 exit code를 확인
echo '{"tool_input":{"command":"rm -rf /"}}' | bash hook.sh > /dev/null 2>&1
echo $?
...
# 안전한 명령어가 통과하는지도 확인
echo '{"tool_input":{"command":"ls -la"}}' | bash hook.sh > /dev/null 2>&1
echo $?
...

3가지 패턴을 모두 테스트한다.

테스트입력 예시기대하는 exit code
차단 대상rm -rf /2
경고 대상(디스크 80%)1
정상 통과ls -la0

테스트 없이 배포하는 hook은 문을 잠그는 것을 잊은 문과 같다.

hook을 작성할 때는 다음과 같이 생각하라.

이 작업을 중단시키고 싶은가?exit 2

사용자에게 알리고 싶지만 중단시키고 싶지는 않은가?exit 1

(+ stderr)

아무것도 하지 않아도 되는가?exit 0

자동 승인하고 싶은가? → stdout에 JSON + exit 0

망설여진다면 exit 2를 선택하라. 안전한 쪽을 택하는 것이 정답이다. 그냥 통과시켜 버리면 돌이킬 수 없다.

446의 hook 예제로 실습: npx cc-safe-setup (6,099개 테스트 포함)

체계적으로 배우기: Claude Code Hooks 실전 가이드 (Zenn Book)

여러분의 hook에서 exit 1exit 2를 실수했던 경험이 있나요? 댓글로 알려주세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0