Hook Stop в Claude Code запускает команду каждый раз, когда основной агент Claude заканчивает ответ, поэтому именно здесь удобно отправлять уведомление о завершении, логировать результат или запускать проверки. Он не срабатывает, если Вы прерываете Claude, а если ход завершается ошибкой API, вместо него срабатывает StopFailure. Hook получает последнее сообщение Claude в last_assistant_message и может вернуть decision: "block" с полем reason, чтобы Claude продолжил работу, а не остановился.
01Когда Stop срабатывает, а когда нет
Stop срабатывает в конце каждого хода, а не только когда вся задача выполнена. Claude Code ничего не знает о «задаче» — он знает лишь, что Claude закончил отвечать и ждёт Вас. Короткий вопрос даёт один Stop, как и двухчасовой рефакторинг.
Он не срабатывает:
- когда Вы нажимаете Esc или иным образом прерываете ход;
- когда ход завершается ошибкой API, например из-за rate limit, перегрузки или ошибки аутентификации. В этих случаях Claude Code вызывает
StopFailureс полемerror, по которому можно фильтровать (rate_limit,overloaded,server_errorи другие); - когда завершается субагент. Для этого есть
SubagentStop.
Stop не поддерживает matcher, поэтому любой добавленный matcher игнорируется, и hook срабатывает на каждом ходе.
02Простейший Stop hook
Добавьте это в ~/.claude/settings.json, чтобы запускать команду каждый раз, когда Claude заканчивает работу. Этот пример проигрывает звук в macOS:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}
Команды для уведомлений на рабочем столе в Mac, Windows и Linux описаны в статье как получать уведомление, когда Claude Code заканчивает работу. Далее в этом руководстве речь пойдёт о том, что можно делать со входными и выходными данными hook.
03Что получает hook
Помимо общих полей (session_id, transcript_path, cwd, permission_mode, hook_event_name), hook Stop получает:
last_assistant_messageТекст последнего ответа Claude в этом ходеstop_hook_activetrue, если Claude уже продолжает работу, потому что Stop hook заблокировал остановкуbackground_tasksЕщё выполняющиеся задачи, например фоновые shell или субагентыsession_cronsЗапланированные пробуждения в этой сессии, например от /loopИспользуйте last_assistant_message, а не читайте файл транскрипта. Транскрипт записывается асинхронно и к моменту запуска 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 продолжать работу.
05Как заставить Claude продолжить
Stop — одно из событий, которые можно блокировать. Если заблокировать остановку, Claude продолжит ход, используя Вашу причину как следующую инструкцию. Сделать это можно двумя способами:
- завершиться с кодом
2— тогда Claude получит Ваш stderr в качестве причины; или - завершиться с кодом
0и вывести JSON:
{
"decision": "block",
"reason": "The test suite is failing. Run npm test, fix the failures, then finish."
}
Если hook даёт указания, а не сообщает об ошибке, разговор также продолжит hookSpecificOutput.additionalContext, но в транскрипте это будет помечено как отзыв Stop hook, а не как ошибка hook:
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": "Please run the test suite before finishing"
}
}
Как избежать бесконечного цикла
Stop hook, который всегда блокирует, никогда не даст Claude остановиться. Защищают от этого две вещи:
stop_hook_activeпринимает значениеtrue, когда Claude уже продолжает работу из-за Stop hook. Проверяйте его и сразу выходите.- Claude Code всё равно завершает ход, если Stop hooks заблокировали остановку восемь раз подряд без единого вызова инструмента между ними. Этот лимит можно повысить переменной окружения
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP.
Вот hook, который один раз заставляет Claude исправить падающие тесты, а затем позволяет ему остановиться:
#!/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
Hooks запускаются в рабочей директории сессии, поэтому npm test выполняется в Вашем проекте. Делайте проверки быстрыми: по умолчанию command hook может работать до 600 секунд, и Claude всё это время ждёт.
Если Вы просто хотите, чтобы Claude продолжал работать над целью без написания скрипта, встроенная команда /goal в Claude Code сама настроит Stop hook в рамках сессии.
06Уведомления и об ошибках
Поскольку при ошибках API Stop не срабатывает, уведомление «готово» не сообщит, что Claude остановился из-за rate limit. Чтобы узнавать об этом, добавьте тот же скрипт в StopFailure. Его вывод и код выхода игнорируются, так что он может только уведомлять или писать в лог. Для StopFailure поле last_assistant_message содержит текст ошибки, например "API Error: Rate limit reached".
07Место Claude Code Notifier
Stop hook, вызывающий push-сервис, — вполне хороший способ узнать, что Claude закончил, пока Вас нет рядом. Если хотите настроить это сами, начните с руководства по ntfy.
Claude Code Notifier использует тот же hook Stop, но с одним отличием: он может отправить ответ обратно. Его бесплатная утилита командной строки ccn работает на Mac, Windows и Linux и сама показывает уведомление на рабочем столе. Если подключено приложение для iPhone или Android, Вы получите push «Claude has finished» сразу по завершении хода и сможете ввести следующую инструкцию прямо с телефона. ccn возвращает её из hook Stop как следующую инструкцию для Claude, и сессия продолжается без Вас за клавиатурой. Если Вы что-то вводили за последние 30 секунд, ожидания ответа с телефона не будет. Это независимый продукт, не созданный Anthropic. Об этом сценарии читайте в статье о длинных задачах без присмотра, а установка описана на странице ccn.
08FAQ
Stop hook срабатывает, когда Claude завершает всю задачу, или после каждого ответа?
После каждого ответа. Stop срабатывает всякий раз, когда основной агент заканчивает отвечать, независимо от объёма запроса. Если нужно понять, запланирована ли ещё работа, проверьте background_tasks и session_crons во входных данных.
Почему мой Stop hook не сработал?
Возможно, Вы прервали ход — тогда Stop пропускается, — или он завершился ошибкой API, и вместо него сработал StopFailure. В остальных случаях убедитесь, что /hooks показывает Ваш hook и что скрипт исполняемый. Остальное описано в чек-листе по устранению неполадок.
Почему Claude продолжает работать после запуска моего Stop hook?
Ваш hook блокирует остановку: либо завершается с кодом 2, либо выводит decision: "block". Убедитесь, что hook, который только уведомляет, всегда завершается с 0, а любой блокирующий hook сначала проверяет stop_hook_active.
Как получить финальный ответ Claude в hook?
Прочитайте last_assistant_message из JSON на stdin, например с помощью jq -r '.last_assistant_message'. Не полагайтесь на transcript_path: в момент срабатывания Stop последнего сообщения в файле может ещё не быть.
Чем Stop отличается от SubagentStop?
Stop относится к основному агенту. SubagentStop срабатывает, когда завершается субагент, фильтруется по типу агента и поддерживает те же поля блокировки. Уведомление «Claude has finished» стоит вешать на Stop.