Claude Code의 Stop hook은 메인 Claude 에이전트가 응답을 마칠 때마다 명령을 실행해요. 그래서 "완료" 알림을 보내거나, 결과를 기록하거나, 검사를 돌리기에 딱 맞는 곳이에요. Claude를 중단시키면 실행되지 않고, API 오류로 끝난 턴에서는 대신 StopFailure가 발생해요. hook은 Claude의 마지막 메시지를 last_assistant_message로 받고, decision: "block"과 reason을 반환하면 Claude가 멈추지 않고 계속 작업하게 할 수 있어요.
01Stop이 실행되는 경우와 실행되지 않는 경우
Stop은 전체 작업이 끝났을 때만이 아니라 매 턴이 끝날 때마다 실행돼요. Claude Code는 "작업"이라는 개념을 모르고, Claude가 응답을 멈추고 사용자를 기다린다는 것만 알아요. 짧은 질문 하나에도 Stop이 한 번, 두 시간짜리 리팩터링에도 한 번이에요.
다음 경우에는 실행되지 않아요.
- Esc를 누르는 등 턴을 중단했을 때
- rate limit, 과부하, 인증 실패 같은 API 오류로 턴이 끝났을 때. 이때 Claude Code는
StopFailure를 발생시키며, 매칭에 쓸 수 있는error필드(rate_limit,overloaded,server_error등)가 함께 와요. - 서브에이전트가 끝났을 때. 이 경우는
SubagentStop이에요.
Stop은 matcher를 지원하지 않아요. matcher를 추가해도 무시되고 hook은 매 턴 실행돼요.
02가장 간단한 Stop hook
Claude가 끝날 때마다 명령을 실행하려면 ~/.claude/settings.json에 다음을 추가하세요. 이 예시는 macOS에서 소리를 재생해요.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}
Mac, Windows, Linux용 데스크톱 알림 명령은 Claude Code가 끝났을 때 알림 받는 방법을 참고하세요. 이 가이드의 나머지 부분에서는 hook의 입력과 출력으로 할 수 있는 일을 다뤄요.
03hook이 받는 데이터
공통 필드(session_id, transcript_path, cwd, permission_mode, hook_event_name) 외에 Stop hook은 다음을 받아요.
last_assistant_message이번 턴에서 Claude가 보낸 마지막 응답 텍스트stop_hook_activeStop hook이 차단해서 Claude가 이미 계속 작업 중이면 truebackground_tasks백그라운드 셸이나 서브에이전트처럼 아직 실행 중인 작업session_crons/loop 등으로 이 세션에 예약된 wake-uptranscript 파일을 읽지 말고 last_assistant_message를 사용하세요. transcript는 비동기로 기록되기 때문에 hook이 실행될 때 마지막 메시지가 아직 없을 수 있어요.
background_tasks와 session_crons를 보면 세션이 정말 끝났는지, 아니면 백그라운드 작업이 다시 깨울 때까지 잠시 멈춘 건지 알 수 있어요. 알림 스크립트에서 둘 다 비어 있는지 확인한 뒤에 "완료"라고 알리면 돼요.
04알림에 Claude의 마지막 메시지 넣기
이 스크립트(macOS, Linux용, jq 필요)는 Claude 응답의 앞부분을 보여 주고, 백그라운드 작업이 아직 실행 중이면 알림을 보내지 않아요.
#!/bin/bash
# ~/.claude/hooks/notify-done.sh
input=$(cat)
# Skip if background tasks are still in flight
busy=$(jq '(.background_tasks // []) | length' <<<"$input")
[ "$busy" -gt 0 ] && exit 0
msg=$(jq -r '(.last_assistant_message // "Claude has finished")
| gsub("\\s+"; " ") | .[0:140]' <<<"$input")
if command -v osascript >/dev/null 2>&1; then
osascript -e 'on run argv' \
-e 'display notification (item 1 of argv) with title "Claude has finished"' \
-e 'end run' "$msg"
elif command -v notify-send >/dev/null 2>&1; then
notify-send "Claude has finished" "$msg"
fi
exit 0
저장한 뒤 chmod +x ~/.claude/hooks/notify-done.sh를 실행하고 hook이 이 스크립트를 가리키게 하세요.
{
"hooks": {
"Stop": [
{
"hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-done.sh" }]
}
]
}
}
이 스크립트는 항상 0으로 종료해요. 이게 중요한데, Stop에서 종료 코드 2는 "멈추지 마"라는 뜻이라서, 알림 스크립트가 실수로 2로 종료하면 Claude가 계속 작업하게 돼요.
05Claude가 계속 작업하게 하기
Stop은 차단할 수 있는 이벤트 중 하나예요. 멈춤을 차단하면 Claude는 지정한 reason을 다음 지시로 삼아 턴을 이어 가요. 방법은 두 가지예요.
2로 종료하면 Claude가 stderr 내용을 reason으로 받아요. 또는0으로 종료하고 JSON을 출력하세요.
{
"decision": "block",
"reason": "The test suite is failing. Run npm test, fix the failures, then finish."
}
hook이 오류를 알리는 게 아니라 안내를 주는 경우라면 hookSpecificOutput.additionalContext로도 대화를 이어 갈 수 있어요. 이때 transcript에는 hook 오류가 아니라 Stop hook 피드백으로 표시돼요.
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": "Please run the test suite before finishing"
}
}
무한 루프 피하기
항상 차단하는 Stop hook은 Claude를 절대 멈추지 못하게 해요. 다음 두 가지가 이를 막아 줘요.
- Stop hook 때문에 Claude가 이미 계속 작업 중이면
stop_hook_active가true가 돼요. 이 값을 확인하고 일찍 종료하세요. - 도구 호출 없이 Stop hook이 연속 8번 차단하면 Claude Code는 어쨌든 턴을 끝내요. 이 한도는
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP환경 변수로 늘릴 수 있어요.
다음은 Claude에게 실패한 테스트를 한 번 고치게 한 뒤 멈추도록 하는 hook이에요.
#!/bin/bash
# ~/.claude/hooks/tests-must-pass.sh
input=$(cat)
[ "$(jq -r '.stop_hook_active' <<<"$input")" = "true" ] && exit 0
if ! npm test --silent >/dev/null 2>&1; then
jq -nc '{decision: "block", reason: "npm test is failing. Fix the failures, then finish."}'
fi
exit 0
Hook은 세션의 작업 디렉터리에서 실행되므로 npm test는 프로젝트 안에서 실행돼요. 검사는 빠르게 유지하세요. 기본적으로 command hook은 최대 600초까지 실행될 수 있고, 그동안 Claude는 기다려요.
스크립트를 작성하지 않고 Claude가 목표를 향해 계속 작업하게 하고 싶다면, Claude Code에 내장된 /goal 명령이 세션 범위의 Stop hook을 대신 설정해 줘요.
06실패 시에도 알림 받기
API 오류는 Stop을 건너뛰기 때문에, "완료" 알림으로는 Claude가 rate limit 때문에 중단했다는 걸 알 수 없어요. 이런 경우에도 알림을 받고 싶다면 같은 스크립트를 StopFailure에도 추가하세요. 출력과 종료 코드는 무시되므로 알림이나 로그 용도로만 쓸 수 있어요. StopFailure에서는 last_assistant_message에 "API Error: Rate limit reached" 같은 오류 메시지가 담겨요.
07Claude Code Notifier의 역할
자리를 비운 동안 Claude가 작업을 끝냈는지 알고 싶다면, push 서비스를 호출하는 Stop hook으로 충분해요. 직접 구성하고 싶다면 ntfy 가이드부터 시작해 보세요.
Claude Code Notifier도 같은 Stop hook을 사용하지만, 한 가지 다른 점이 있어요. 응답을 돌려보낼 수 있다는 거예요. 무료 커맨드라인 도구인 ccn은 Mac, Windows, Linux에서 동작하며 자체적으로 데스크톱 알림을 띄워요. iPhone 또는 Android 앱을 연결하면 턴이 끝나는 즉시 "Claude has finished" push를 받고, 휴대폰에서 다음 지시를 입력할 수 있어요. ccn이 이를 Stop hook에서 Claude의 다음 지시로 반환하기 때문에, 키보드 앞에 없어도 세션이 계속 진행돼요. 최근 30초 안에 직접 입력했다면 휴대폰 응답을 기다리지 않아요. 이 도구는 독립적으로 만들어졌으며 Anthropic이 만든 것이 아니에요. 이 워크플로는 긴 작업 중 자리 비우기를, 설치는 ccn 페이지를 참고하세요.
08FAQ
Stop hook은 Claude가 전체 작업을 끝냈을 때 실행되나요, 아니면 응답할 때마다 실행되나요?
응답할 때마다 실행돼요. Stop은 요청 규모와 상관없이 메인 에이전트가 응답을 마칠 때마다 발생해요. 아직 예정된 작업이 남아 있는지 알고 싶다면 입력의 background_tasks와 session_crons를 확인하세요.
Stop hook이 왜 실행되지 않았나요?
턴을 중단했다면 Stop이 건너뛰어지고, API 오류로 끝났다면 대신 StopFailure가 발생해요. 그 외에는 /hooks에 hook이 표시되는지, 스크립트에 실행 권한이 있는지 확인하세요. 나머지는 문제 해결 체크리스트에서 다뤄요.
Stop hook이 실행된 뒤에도 Claude가 계속 작업하는 이유는 무엇인가요?
hook이 코드 2로 종료하거나 decision: "block"을 출력해서 종료를 막고 있는 거예요. 알림 전용 hook은 항상 0으로 종료하도록 하고, 차단하는 hook은 먼저 stop_hook_active를 확인하도록 하세요.
hook에서 Claude의 최종 답변은 어떻게 가져오나요?
stdin으로 들어오는 JSON에서 last_assistant_message를 읽으세요. 예를 들어 jq -r '.last_assistant_message'를 쓸 수 있어요. transcript_path에 의존하지 마세요. Stop이 발생하는 시점에는 파일에 마지막 메시지가 아직 없을 수 있어요.
Stop과 SubagentStop의 차이는 무엇인가요?
Stop은 메인 에이전트용이에요. SubagentStop은 서브에이전트가 끝날 때 발생하고, 에이전트 유형으로 매칭하며, 같은 차단 필드를 지원해요. "Claude has finished" 알림은 Stop에 두세요.