Claude Code가 종료 훅 뒤 계속 작업할 때: 반복 방지와 완료 검증 구분하기
By ON Lab · Sources checked November 8, 2026
Claude Code의 Stop 훅에서 decision: "block"을 반환하면 종료를 막고 작업을 계속하게 합니다. 같은 미완료 조건을 매번 반환하면 반복이 생길 수 있으므로 stop_hook_active를 확인하고, 조건이 실제로 해결 가능한지부터 검토하세요. 반복 상한 때문에 턴이 끝나도 테스트나 사업 작업이 성공했다는 뜻은 아닙니다.
실제 독자는 무엇을 혼동했나요?
연결한 공개 이슈에는 종료 전 검증을 강제하는 훅을 시험하다 반복 차단 상한을 만났다는 사용자 보고가 있습니다. 최초 문제 제기는 문서에 상한과 환경변수 설명이 없다는 내용이었고, 뒤의 확인 댓글은 문서가 보완됐다고 기록합니다. 따라서 지금도 문서가 빠져 있다고 소개하거나 과거 보고를 현재 모든 버전의 결함으로 일반화하지 않습니다.
현재 공식 문서는 Stop 입력, 계속 실행 판단, 연속 상한을 함께 설명합니다. 이 글의 목적은 그 세 부분을 운영 순서로 연결하는 것입니다. 기존 권한 프롬프트 안내나 PreToolUse의 도구 실행 차단과 달리, 여기서는 응답을 끝내려는 시점의 계속 실행을 다룹니다.
종료 이벤트와 실패 이벤트부터 나누세요
Stop은 주 에이전트가 응답을 마쳤을 때 발생하며 사용자 인터럽트로 멈춘 경우에는 실행되지 않습니다. API 오류로 턴이 끝나면 Stop 대신 StopFailure가 발생합니다. 인증 실패나 사용량 제한으로 멈춘 작업에 대해 Stop 훅의 재시도 조건만 고쳐서는 실제 원인을 확인할 수 없습니다.
이벤트 이름, Claude Code 버전, 훅이 받은 입력과 내보낸 결과를 한 회차의 기록으로 묶으세요. 로그에서 API 오류인지 정상 응답 종료인지 구분하고, 비밀정보가 있는 전체 대화나 인증 헤더를 공개하지 마세요. StopFailure는 공식 문서상 결정 제어를 지원하지 않으므로 Stop의 block 출력을 그대로 복사해 복구를 기대하지 않습니다.
반환값에 따라 달라지는 의미
| 출력·입력 | 현재 문서의 의미 | 검수할 점 |
|---|---|---|
| decision: block과 reason | 종료를 막고 reason을 전달 | 구체적으로 해결 가능한 사유인지 확인 |
| decision 생략 | 해당 훅은 종료를 허용 | 검증 통과를 자동 증명하지 않음 |
| 종료 코드 2와 stderr | 계속해야 하는 사유로 전달 | 실제 종료 코드와 stderr를 별도 확인 |
| additionalContext | 오류 표시 없이도 대화를 계속함 | 같은 반복 보호가 적용됨 |
| stop_hook_active: true | 종료 훅 때문에 이미 계속 중 | 같은 조건의 재차 차단 방지 |
표의 decision 생략은 다른 훅이 없거나 다른 차단도 없다는 보장이 아닙니다. 여러 설정이나 플러그인에서 Stop 훅을 등록했다면 실제 실행된 훅을 각각 확인하세요. 눈앞의 스크립트 하나가 종료를 허용해도 다른 훅이 계속 실행을 요청할 수 있습니다.
여덟 번 상한을 전체 실행 예산으로 쓰지 마세요
확인한 공식 문서는 연속 계속 실행이 여덟 번 발생하면 다음 차단을 무시하고 턴을 끝내며, 도구를 호출할 때 연속 횟수가 초기화된다고 설명합니다. 따라서 이것을 전체 세션의 도구 호출 여덟 번 제한이나 전체 반복 여덟 번 종료 보장으로 계산하면 안 됩니다. 중간에 도구 호출이 있는 흐름과 도구 없이 차단만 반복되는 흐름을 구분해야 합니다.
문서에는 상한을 조정하는 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP도 나옵니다. 원인 확인 없이 상한부터 늘리면 해결되지 않는 조건을 더 오래 반복할 수 있습니다. 사람의 인증·명시적 승인·권한 변경이 필요한 상태라면 필요한 입력과 재개 경로를 기록하고 종료하도록 설계하세요. 훅이 사용자 대신 승인이나 인증을 만들 수는 없습니다.
순수 판단 함수로 먼저 확인할 네 가지 경우
아래는 훅 설치 파일 전체가 아닌 Python 판단 함수입니다. checks_passed는 호출자가 실제 검증 결과에서 계산해 전달하는 함수 인자이며 Claude Code의 표준 입력 필드가 아닙니다. 외부 검증 실행이나 결과 저장을 이 함수가 대신하지 않습니다.
def stop_decision(event, checks_passed):
if event.get("stop_hook_active") is True:
return {}
if checks_passed:
return {}
return {
"decision": "block",
"reason": "Local verification has not passed; inspect the recorded failure."
}
우리는 일반 Python에서 입력 두 값의 네 조합을 실행했습니다. 이미 계속 실행 중이 아니고 검증도 통과하지 않은 경우에만 block을 반환했고, 나머지 세 경우는 빈 객체를 반환했습니다. 이것은 함수 분기 검증이며 실제 Claude 세션에서 상한이나 플러그인 간 상호작용을 재현한 결과는 아닙니다.
이 예시는 반복 방지를 우선하는 설명용 구조입니다. 두 번째 종료에서는 검증이 실패해도 차단을 반복하지 않으므로, 이를 강제 품질 게이트로 사용하면 안 됩니다. 실패한 검증은 별도 기록과 실패 상태로 남기고, 미완료 작업이 무엇인지 사용자나 다음 실행자가 읽을 수 있어야 합니다. “턴 종료 허용”과 “결과 통과”를 다른 값으로 관리하는 이유입니다.
운영 훅에 적용하기 전 확인 순서
- 원본 설정을 보존하고 실제 적용되는 Stop 훅과 버전을 확인합니다.
- 훅의 입력 JSON에서 이벤트와 stop_hook_active를 읽습니다. 파싱 실패를 검증 성공으로 처리하지 않습니다.
- 현재 실패를 에이전트가 승인 범위 안에서 해결할 수 있는지 판단합니다.
- 검증 결과에 시각과 해당 실행의 식별 근거를 연결합니다. 다른 세션의 오래된 성공 파일을 재사용하지 않습니다.
- 계속 실행 사유와 종료 사유를 별도 기록하고 제한된 시험 세션에서 관찰합니다.
- 턴이 끝난 후 실제 변경 내용·테스트 결과·남은 장애를 다시 확인합니다.
실패를 재현할 때 모델을 무한히 실행하거나 실제 권한 거부를 우회할 필요는 없습니다. 먼저 순수 함수 입력과 저장된 실패 로그를 이용해 반복 조건을 확인하세요. 실제 세션 시험이 필요하면 시간과 반복 상한을 미리 정하고 사용자 인터럽트가 동작하는지도 확인합니다.
계속 반복될 때 남길 기록
버전, 이벤트 이름, 발생 시각, 활성 훅 목록, 각 회차의 stop_hook_active, 반환 decision·reason, 종료 코드, 마지막 도구 호출 시각을 남기면 반복 상한이 초기화됐는지 검토할 수 있습니다. 원인 미확인 상태를 “고쳤다”로 바꾸지 말고, 다음 확인을 담당할 사람과 필요한 증거를 연결하세요.
도구 실행 전 차단을 조사 중이라면 PreToolUse 종료 코드와 stderr 안내가 다른 이벤트의 분류를 돕습니다. 두 훅의 판단 필드를 서로 바꾸어 적용하지 마세요.
2026년 11월 8일 공식 문서와 독자 원문을 확인했습니다. 직접 실행한 범위는 Python 판단 함수의 네 입력 조합입니다. 실제 Claude 종료 훅 설치·API 오류·연속 상한·사용자 환경은 재현하지 않았습니다.