Claude Code Stop hook 예제: "done" 클레임 확인하기
요약
본 문서는 Claude Code의 Stop hook 기능을 심층적으로 다루며, 에이전트가 응답을 마칠 때 실행되는 훅의 작동 방식을 설명합니다. 특히 `last_assistant_message`와 같은 페이로드 필드를 활용하여, 단순히 코드가 작성되었는지 확인하는 것을 넘어 변경된 파일 목록과 실제 코드 변화를 검증하는 고급 방법을 제시합니다.
핵심 포인트
- Stop hook은 Claude가 응답을 마칠 때 발생하며, JSON 페이로드를 통해 정보를 받습니다.
- 단순히 텍스트만 읽는 것보다 `last_assistant_message` 필드가 핵심 클레임입니다.
- 최적의 방법은 에이전트의 마지막 메시지를 `git diff`와 비교하여 변경된 파일 언급 여부를 확인하는 것입니다.
Claude Code Stop hook 예제를 검색하면 두 가지 종류를 얻을 수 있습니다. 하나는 에이전트가 멈출 때 테스트를 실행합니다. 다른 하나는 소리를 재생합니다. 둘 다 괜찮습니다. 어느 것도 에이전트가 방금 작성한 단락을 읽지는 않습니다.
하지만 이 예제는 그렇습니다. 에이전트의 마지막 메시지를 가져와 git diff와 비교하고, 변경된 파일이 언급되지 않은 채로 에이전트가 멈추도록 허용하지 않습니다. 의존성 없이 Node 코드 31줄입니다. 아래 출력은 모두 실제 실행 결과입니다.
테스트 게이트는 코드를 확인합니다. 당신이 병합하려는 단락을 아무것도 확인하지 않습니다.
Stop hook가 볼 수 있는 것들
Stop은 Claude가 응답을 마칠 때 발생합니다. 사용자의 명령어는 stdin으로 JSON 페이로드를 받습니다. 여기는 제 hook에 공급된 내용이며, 경로는 짧게 줄였습니다:
$ cat ../stop-payload.json
{
"session_id": "ed45f42f-…",
...
네 가지 필드가 중요합니다:
-
last_assistant_message: 이것이 클레임입니다. 트랜스크립트에서 추출할 필요가 없습니다. Anthropic의 문서는 최종 텍스트를 원하는 hook은 이 필드를 사용해야 한다고 말합니다. -
transcript_path: 마지막 메시지만으로는 충분하지 않을 때 전체 세션을 JSONL 형식으로 제공합니다. -
cwd:git을 실행할 위치입니다. -
stop_hook_active: 이것이 핵심입니다. 아래에서 더 자세히 설명하겠습니다.
저는 이 페이로드를 수동으로, 문서화된 형태에 맞춰, 수정 사항을 만든 Claude Code 세션의 실제 트랜스크립트로부터 만들었습니다. 저장된 페이로드를 파이프하면 기다릴 필요 없이 hook을 테스트하는 가장 빠른 방법이기도 합니다.
일반적인 예제: 테스트 게이트와 그것이 놓치는 것들
과제는 작았습니다. auth.js의 login() 함수가 사용자가 정의되지 않았을 때 오류를 발생시켰습니다. Claude Code는 가드 라인 하나를 추가하고 다음과 같이 말했습니다:
완료되었습니다. auth.js의 login()은 이제 user가 undefined일 때 .trim()에서 오류를 발생시키는 대신 false를 반환합니다. 다른 변경 사항 없이 가드 라인 하나만 필요합니다.
작업 디렉토리는 다음과 같습니다:
$ git status --short
M auth.js
M config.js
config.js는 과제가 시작되기 전에 의도적으로 수정되었습니다. 저는 세 번 전 작업의 편집을 대신하기 위해 여기에 0으로 설정된 타임아웃을 남겨두었습니다. 이제 테스트를 해보겠습니다:
$ node --test 2>&1 | grep -E "^# (pass|fail)"
pass 2
fail 0
Green. 평소의 Stop hook은 테스트 스위트를 실행하고 실패 시 2를 반환하여, 문서에서 말하는 것처럼 "Claude가 중지되는 것을 방지"합니다. 여기서는 실패할 것이 아무것도 없습니다. Claude는 중지되고, 당신이 커밋하며, 제로 타임아웃과 함께 버그 수정이 배포됩니다.
테스트 게이트를 유지하세요. 그것은 실제 질문에 답합니다. 다만 이 질문은 아닙니다.
더 나은 방법: 요약을 diff와 대조하기
같은 이벤트, 다른 질문: 메시지가 변경된 모든 파일을 명시했는가?
이것을 .claude/hooks/check-done.mjs로 저장하세요:
#!/usr/bin/env node
// Stop hook: Claude가 마지막 메시지에서 변경한 모든 파일 이름을 언급할 때까지 중지되지 않도록 합니다.
import { readFileSync } from 'node:fs';
...
ls-files --others 부분이 중요합니다. git diff만으로는 완전히 새로운 파일을 절대 나열하지 못하므로, 이것이 없으면 완전히 새로운 파일 하나가 그냥 지나치게 됩니다.
.claude/settings.json에 등록하고 둘 다 커밋하여 팀 전체가 이를 알도록 하세요:
{
"hooks": {
"Stop": [
...
문서에서 가져온 두 가지 세부 사항입니다. Stop은 matcher를 받지 않습니다. 하나를 추가하면 조용히 무시됩니다. 그리고 args는 훅을 실행(exec) 형태로 전환하며, 그 사이에 셸이 없습니다. 이는 문서가 경로 플레이스홀더와 관련될 때 권장하는 방식입니다. 공백이 포함된 프로젝트 경로는 명령어를 두 개로 나눌 수 없습니다.
같은 페이로드, 같은 레포지토리:
$ node .claude/hooks/check-done.mjs < ../stop-payload.json
{"decision":"block","reason":"완료했다고 했지만 config.js를 언급하지 않았습니다. 각각 무엇이 변경되었는지 말하거나, 되돌리세요."}
"decision": "block"은 Claude가 계속 작업하도록 유지하고, reason은 다음 지침이 됩니다. 테스트는 config.js에 대해 할 말이 아무것도 없었습니다. 이것(훅)은 했습니다.
만약 훅 오류로 나타나는 블록이 거슬린다면, 문서는 대신 hookSpecificOutput.additionalContext를 제공합니다. 동일한 루프 보호 기능이지만, 전사본에는 "Stop hook feedback"으로 레이블링됩니다.
무한 루프 함정
훅이 블록되면, Claude는 답변을 한 다음 다시 멈추려고 시도합니다. 이것은 Stop hook을 다시 발동시킵니다. 만약 훅이 같은 질문을 하고 같은 답변을 받으면, 다시 블록됩니다.
해결책은 stop_hook_active입니다. 문서는 이것이 'Claude Code가 stop hook의 결과로 이미 계속 진행 중일 때' 참이라고 설명합니다. 그것이 스크립트의 두 번째 줄입니다. 플래그를 설정하여 다음과 같습니다:
$ grep stop_hook_active ../stop-payload-active.json
"stop_hook_active": true,
$ node .claude/hooks/check-done.mjs < ../stop-payload-active.json; echo "exit $?"
...
출력이 없고, exit 0이 나오면 Claude가 멈춥니다. 훅은 멈출 때마다 한 번씩 블록을 받습니다. 만약 Claude의 답변이 미흡하더라도 여전히 멈추고, 사용자는 그 답변을 읽게 됩니다. 이것이 트레이드오프이며, 올바른 방식입니다.
이 줄이 없으면, 문서는 안전장치를 설명합니다: Claude Code는 stop hook이 한 번에 연속으로 턴(turn)을 여덟 번 계속 진행한 후 다음 블록을 무시합니다. 이에 의존하지 마십시오. 카운트는 Claude가 도구를 호출할 때마다 초기화되며, diff를 확인하는 것은 도구 호출을 의미합니다. 저는 이를 증명하기 위해 세션을 중단시키지 않았습니다. 이 가드레일은 한 줄의 비용이 듭니다.
또는 플러그인이 더 많은 역할을 하도록 허용하기
trust issues는 제가 같은 이벤트에 대해 만든 무료 플러그인입니다. 이 플러그인은 transcript_path에서 클레임을 읽어와 한 번 대신 네 가지 검사를 실행합니다: 언급되지 않았지만 변경된 내용, 삭제된 주장과 같은 조용한 컷(quiet cuts), .skip과 같은 뮤트(mutes), 그리고 발생하지 않은 작업 설명. 페이로드는 동일하며, TI는 플러그인의 stop-check.mjs를 가리킵니다:
$ node "$TI" < ../stop-payload.json | node -pe "JSON.parse(require("fs").readFileSync(0)).systemMessage"
trust issues — the diff says otherwise:
...
기본적으로 사용자에게 알려줄 뿐 블록하지 않습니다. 첫 번째 오경보에 의해 중단되는 가드레일은 같은 오후에 제거됩니다. 신뢰할 경우, TRUST_ISSUES=strict로 설정하면 Claude가 대신 비용을 지불하게 합니다:
$ TRUST_ISSUES=strict node "$TI" < ../stop-payload.json
{"decision":"block","reason":"이 done을 호출하기 전에 — 요약과 diff가 일치하지 않습니다:\n\nplumb — 2개의 파일 변경, 요약에 이름 언급된 것은 1개\n\n변경되었지만 언급되지 않은 것 (먼저 읽어보세요)\n · config.js 수정됨\n\n요약이 알려주지 않은 한 가지.\n\n조용히 변경된 것을 수정하거나, 무엇을 했고 왜 했는지 명확하게 말하세요."}```
`stop_hook_active: true` 페이로드에서는 두 모드 모두 0으로 종료되며 아무것도 출력하지 않습니다.
## 이것이 하지 않는 것들
- **문자 그대로 이름을 일치시킵니다.** "auth.js"는 카운트됩니다. "The auth module"은 그렇지 않습니다. 에이전트에게 모든 작업을 파일 목록으로 끝내도록 요청하면 노이즈가 사라집니다.
- **턴(turn)이 아닌 작업 디렉터리(working tree)를 확인합니다.** 이것이 `config.js`를 포착한 방식입니다. 또한, 커밋할 때까지 이전 작업의 파일들에 대해 계속해서 알려줄 것입니다.
- **언급하는 것이 진실은 아닙니다.** "테스트 업데이트"는 파일을 이름으로 언급하고 `.skip`을 숨깁니다. 이것이 trust issues에서 quiet-cut 및 mute 검사가 하는 일입니다.
- **코드가 작동하는지 알려줄 수 없습니다.** 테스트 게이트도 실행하세요. 둘 다 Stop에 함께 있을 수 있습니다.
요약이 애초에 무언가를 누락시키는 이유에 대한 글은 별도로 작성했습니다: [Claude Code가 완료되었다고 말할 때. 단락이 아닌 diff를 확인하세요.](https://singhlabs.dev/blog/claude-code-says-done/)
**이것이 우리가 클라이언트를 위한 에이전트를 구축하는 방식입니다.** 에이전트가 끝났는지에 대한 마지막 말을 할 권한을 갖지 않습니다. 작업을 수행하지 않은 것이 무엇을 주장했는지와 무엇이 변경되었는지를 확인합니다.
출처: 모든 터미널 블록은 throwaway git repo에서 2026년 10월 9일에 실제 실행된 내용입니다 (Node 22.17.1, trust issues 1.0.1). 경로는 단축되었습니다. Claude Code의 명령줄은 이 장치에 로그인되지 않았기 때문에, 실제 Stop으로부터 페이로드가 기록되지는 않았습니다. 두 페이로드 모두 수정 작업을 수행한 Claude Code 세션의 실제 트랜스크립트에서 문서화된 형태로 빌드되었으며, 수동으로 파이프되었습니다. 페이로드 필드, 종료 코드(exit codes), 실행 형식(exec form), `stop_hook_active`, 그리고 8개 연속 제한은 Anthropic의 [hooks reference](https://code.claude.com/docs/en/hooks)에서 가져온 것입니다.
이것이 들어가는 곳: [테스트 및 검증, Agent Ops Stack의 레이어 5](https://singhlabs.dev/agent-ops-stack/#testing).
다음으로 읽을 내용: [Claude Code가 완료되었다고 말합니다. 단락이 아닌 diff를 확인하세요.](https://singhlabs.dev/blog/claude-code-says-done/) · [내 메일러는 “캠페인 전송됨”이라고 인쇄했습니다. 아무에게도 보내지지 않았습니다.](https://singhlabs.dev/blog/a-green-tick-over-nothing/)
_원래 게시된 곳: [singhlabs.dev](https://singhlabs.dev/blog/claude-code-stop-hook-example/)._
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기