이벤트 기반 제어 흐름
훅이 흐름을 어떻게 제어하는지는 두 가지 출력 메커니즘으로 결정됩니다. 하나는 프로세스 종료 코드이고, 다른 하나는 stdout으로 내보내는 구조화된 JSON입니다. 시험은 이 둘의 차이와, 각각이 이벤트별로 어떻게 해석되는지를 집중적으로 묻습니다.
stdin 입력 규약
명령형 훅은 실행될 때 stdin으로 이벤트 정보를 담은 JSON을 받습니다. 여기에는 이벤트 이름, 도구 이름(tool_name), 도구 입력(tool_input) 등이 포함됩니다. 훅 스크립트는 이 입력을 파싱해 어떤 작업이 일어나려는지 판단합니다.
// ~/.claude/hooks/guard.sh#!/bin/bashINPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')if echo "$COMMAND" | grep -qiE '\b(DROP|TRUNCATE|rm -rf)\b'; then echo "차단됨: 파괴적 명령은 허용되지 않습니다" >&2 exit 2fiexit 0
이 예시는 PreToolUse 훅으로, Bash 명령을 검사해 파괴적 패턴이 보이면 차단합니다. jq로 tool_input.command를 추출하는 패턴이 정석입니다. stdout/sed/grep으로 JSON을 직접 다루지 말고 jq를 쓰는 것이 권장됩니다.
종료 코드 기반 제어
가장 단순한 제어 방식은 종료 코드입니다.
- 종료 코드 0: 성공. 훅은 아무 결정도 내리지 않으며, 정상적인 권한 흐름이 그대로 진행됩니다.
- 종료 코드 2: 차단 신호. stderr에 출력한 내용이 Claude에게 피드백으로 전달됩니다. 이벤트에 따라 효과가 달라집니다.
- 그 외 코드: 비차단 오류로 취급되어 stderr가 사용자에게 표시되지만 흐름은 계속됩니다.
종료 코드 2의 효과가 이벤트마다 다르다는 점이 핵심입니다.
- PreToolUse, UserPromptSubmit에서는 해당 도구 호출이나 프롬프트 처리가 차단됩니다.
- Stop, SubagentStop에서는 멈춤이 막혀 모델이 계속 작업합니다.
- PostToolUse 등 이미 행위가 끝난 이벤트에서는 출력과 종료 코드가 무시됩니다. 행위가 이미 일어났기 때문입니다.
시험 포인트: "PostToolUse 훅에서 exit 2로 도구 실행을 취소한다"는 보기는 오답입니다. PostToolUse 시점에는 도구가 이미 실행을 마쳤으므로 되돌릴 수 없습니다. 취소가 목적이면 PreToolUse를 써야 합니다.
구조화된 JSON 출력
종료 코드보다 정교한 제어가 필요하면, stdout으로 JSON을 내보냅니다. JSON 출력은 단순 차단을 넘어 입력 수정, 컨텍스트 주입, 사유 전달까지 가능합니다.
PreToolUse에서는 hookSpecificOutput 안에 permissionDecision을 담아 권한 결정을 내립니다. 값은 allow, deny, ask 등입니다. updatedInput으로 도구 입력 전체를 교체할 수도 있고, additionalContext로 모델에게 추가 정보를 줄 수도 있습니다.
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "프로덕션 DB에 대한 쓰기 작업은 금지됩니다", "additionalContext": "읽기 전용 쿼리로 다시 시도하세요" }}
UserPromptSubmit과 Stop/SubagentStop에서는 최상위 decision과 reason을 씁니다. decision을 block으로 설정하면 프롬프트 처리를 막거나(UserPromptSubmit) 멈춤을 막습니다(Stop). 멈춤을 막을 때는 reason이 반드시 있어야 모델이 무엇을 더 해야 할지 알 수 있습니다.
{ "decision": "block", "reason": "테스트가 아직 통과하지 않았습니다. 실패한 테스트를 먼저 고치세요."}
시험 포인트: 종료 코드 2와 JSON 출력은 둘 다 차단할 수 있지만, JSON 출력만이 입력 수정(
updatedInput)·권한 세분화(allow/deny/ask)·구조화된 사유 전달을 지원합니다. "도구 입력을 검사 후 안전한 값으로 교체한다"는 요구가 나오면 JSON 출력이 정답입니다. 단순히 막기만 한다면 exit 2로도 충분합니다.
HTTP 훅과 관찰성
type: "http" 훅은 이벤트 데이터를 외부 엔드포인트로 POST합니다. 감사 로깅, 외부 알림, 중앙 정책 서버 연동 등 관찰성 용도에 적합합니다. 헤더에 환경 변수를 넣을 때는 allowedEnvVars로 명시적으로 허용해야 합니다.
// .claude/settings.json{ "hooks": { "PostToolUse": [ { "hooks": [ { "type": "http", "url": "http://localhost:8080/hooks/tool-use", "headers": { "Authorization": "Bearer $AUDIT_TOKEN" }, "allowedEnvVars": ["AUDIT_TOKEN"] } ] } ] }}
의사결정 매핑 정리
시험에서 자주 등장하는 시나리오와 정답 이벤트를 묶어 두면 함정을 피하기 쉽습니다.
- 위험한 명령(파괴적 Bash, 프로덕션 쓰기)을 모델 협조 없이 막아야 한다 → PreToolUse + deny 또는 exit 2.
- 도구 입력을 안전하게 정규화·교체한다 → PreToolUse +
updatedInput. - 모든 편집 후 자동 포맷팅·린트를 강제한다 → PostToolUse(관찰·후처리).
- 사용자 입력을 검증하거나 컨텍스트를 주입한다 → UserPromptSubmit.
- 작업이 미완인데 모델이 멈추려 하면 계속하게 한다 → Stop +
decision: "block"+reason. - 모든 이벤트를 중앙 서버에 감사 로깅한다 → HTTP 훅.
정리
- 명령형 훅은 stdin으로 이벤트 JSON을 받고,
jq로tool_input등을 파싱해 판단한다. - 종료 코드 0은 무결정, 2는 차단 신호이며 stderr가 모델 피드백이 된다. PostToolUse처럼 사후 이벤트에서는 무시된다.
- 구조화된 JSON 출력은 차단뿐 아니라
updatedInput(입력 교체),permissionDecision(allow/deny/ask),decision/reason까지 지원한다. - HTTP 훅은 외부 시스템 연동·감사 로깅 등 관찰성 용도에 쓰며, 환경 변수는
allowedEnvVars로 명시 허용한다. - "막기만 하면 되는가, 입력을 바꿔야 하는가, 이미 일어난 일을 기록하는가"를 기준으로 이벤트와 출력 방식을 선택한다.