Почему это важно именно вам
Представьте: Claude помогает вам разбирать папку с документами. В какой-то момент он решает, что файл «устарел» и собирается его удалить. Файл называется договор_2021_финал.docx — и вы, может быть, сами не помните, нужен ли он. Но удаление необратимо. Было бы хорошо, если бы что-то остановило Клода прямо в этот момент — до того, как действие совершилось.
Или другой сценарий: вы настроили агента, который каждую ночь обрабатывает отчёты. В половине случаев он пишет в папку, куда не должен заходить — в архив прошлого квартала, который уже закрыт. Вы узнаёте об этом только утром, когда разбираете, что пошло не так. Хотелось бы, чтобы система сама проверяла перед каждым шагом: «я вообще имею право это делать?»
Третий сценарий, более тонкий. Вы хотите, чтобы Claude каждый раз, когда собирается отправить письмо от вашего имени, добавлял к своим инструкциям уточнение: «напомни себе проверить тон — это идёт руководителю, не коллеге». Не хочется прописывать это в CLAUDE.md навсегда — только для конкретного класса действий, динамически.
Всё это — задачи для hooks с грамотным управлением exit codes и JSON-ответами. Именно сейчас, в фазе 5 курса, мы разбираем, как hook не просто выполняется, но и общается с Claude на языке, который тот понимает.
Что это такое
Hook — это скрипт, который запускается до или после того, как Claude что-то делает. Мы разобрали это в предыдущих уроках. Но скрипт — не немой исполнитель. Он может ответить Claude на специальном языке: «продолжай», «стоп», «продолжай, но учти вот это». Этот язык состоит из двух частей: exit code и JSON в стандартном выводе.
Аналогия из жизни: вы подаёте заявку на командировку. Финансовый отдел может ответить тремя способами. Первый — молча подписать и вернуть. Второй — вернуть с красным штампом «отказано: превышен лимит». Третий — подписать, но дописать внизу от руки: «обратите внимание: гостиница должна быть согласована отдельно». Claude ведёт себя точно так же в зависимости от того, что вернул hook.
Exit code — это число, которое скрипт возвращает в момент завершения. В Unix-мире это стандарт: 0 означает «всё хорошо», не-ноль — что-то пошло не так. Claude Code использует это разграничение, но добавляет своё значение для числа 2.
Три значения, которые важны
0 — успех. Claude смотрит, есть ли JSON в stdout. Если есть — читает и применяет инструкции из него. Если нет — просто продолжает. 2 — блокировка. Claude останавливает действие и показывает пользователю текст из stderr. Это жёсткое «нет». Любое другое число (1, 3, 127...) — ошибка выполнения самого хука. Claude не блокируется, но записывает произошедшее в лог. Действие продолжается.
Как работает на практике
Разберём по порядку: сначала блокировка, потом управление через JSON.
Блокировка через exit code 2
Допустим, вы хотите, чтобы Claude никогда не удалял файлы из папки ~/Documents/Архив/. Это священная территория. Создаёте hook-скрипт guard_archive.sh:
#!/usr/bin/env bash
# Читаем входные данные от Claude (JSON через stdin)
INPUT=$(cat)
# Извлекаем команду, которую собирается выполнить Claude
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
# Проверяем: есть ли в команде путь к архиву
if echo "$COMMAND" | grep -q "$HOME/Documents/Архив"; then
echo "Нельзя изменять файлы в папке Архив. Обратитесь к Павлу." >&2
exit 2
fi
exit 0
Что здесь происходит: Claude собирается выполнить команду. Перед выполнением запускается этот скрипт. Скрипт получает через stdin JSON с описанием действия, извлекает команду, проверяет путь. Если путь ведёт в архив — скрипт выводит сообщение в stderr и завершается с кодом 2. Claude видит этот код, останавливается и показывает пользователю текст из stderr.
stderr, не stdout
Сообщение для пользователя при блокировке должно идти в stderr (>&2). Если вы напишете в stdout — Claude может попытаться это разобрать как JSON и запутаться. Простое правило: пишете для человека — stderr, пишете для Claude — stdout.
В настройках хука (.claude/settings.json) это выглядит так:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/guard_archive.sh"
}
]
}
]
}
}
Теперь каждый раз, когда Claude собирается выполнить bash-команду, скрипт проверяет путь. Попытка тронуть архив блокируется автоматически.
Управление поведением через JSON в stdout
Exit code 2 — это жёсткий запрет. Но иногда нужно что-то тоньше: не остановить, а скорректировать. Для этого используется JSON в stdout при exit code 0.
Claude читает этот JSON и применяет инструкции из него. Вот какие поля он понимает:
continue (boolean) — продолжать ли выполнение. true по умолчанию. Если поставить false — это мягкая остановка, аналог exit code 2, но через JSON.
stopReason (строка) — сообщение, которое Claude покажет, если continue: false. Объяснение для пользователя.
suppressOutput (boolean) — поле принимается, но ни на что не влияет. Полезно для технических действий, которые должны произойти тихо.
additionalContext (строка, внутри hookSpecificOutput) — текст, который попадёт в контекст Claude для этого шага. Это динамическое расширение контекста прямо в момент действия. Не путайте с полем systemMessage: оно показывает предупреждение вам, а Claude его не видит.
permissionDecision (строка, внутри hookSpecificOutput рядом с обязательным hookEventName) — ответ на запрос разрешения. Значения: "allow", "deny", "ask" (спросить пользователя) и "defer" (выйти, не принимая решения). Значений allow_permanent и deny_permanent не существует.
Пример: динамическое предупреждение для писем
Вы хотите, чтобы перед каждым вызовом инструмента записи в файл Claude получал напоминание проверить, не является ли файл финансовым документом. Не всегда — только когда речь идёт о папке Финансы/. Создаёте скрипт finance_reminder.sh:
#!/usr/bin/env bash
INPUT=$(cat)
# Извлекаем путь файла, который собирается изменить Claude
FILE_PATH=$(echo "$INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
# путь и у Write, и у Edit лежит в tool_input.file_path
print(d.get('tool_input', {}).get('file_path', ''))
" 2>/dev/null)
# Если путь содержит папку Финансы — добавляем напоминание
if echo "$FILE_PATH" | grep -q "Финансы"; then
python3 -c "
import json
print(json.dumps({
'continue': True,
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'additionalContext': 'ВАЖНО: этот файл находится в папке Финансы. Перед записью убедись, что изменения не затрагивают итоговые суммы и формулы расчёта. Если неуверен — спроси пользователя.'
}
}))
"
exit 0
fi
exit 0
Что происходит: Claude собирается записать файл. Хук проверяет путь. Если путь ведёт в папку Финансы/ — хук возвращает JSON с полем additionalContext. Этот текст попадает в контекст Claude, и запись выполняется уже с оглядкой на предупреждение.
additionalContext — это разговор с Клодом, systemMessage — с вами
Поле additionalContext кладёт текст в контекст Claude. Поле systemMessage, наоборот, показывает предупреждение вам в интерфейсе, а Claude его не видит. Это как тихая записка ассистенту перед встречей: «напомни руководителю про ограничение бюджета, когда дойдут до пункта 3».
Пример: автоматическое разрешение для конкретной папки
Если Claude каждый раз спрашивает разрешения на чтение файлов из рабочей папки ~/Documents/Отчёты/, это можно автоматизировать:
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('file_path', '') or d.get('path', ''))
" 2>/dev/null)
if echo "$FILE_PATH" | grep -q "$HOME/Documents/Отчёты"; then
python3 -c "import json; print(json.dumps({'hookSpecificOutput': {'hookEventName': 'PreToolUse', 'permissionDecision': 'allow', 'permissionDecisionReason': 'Рабочая папка Отчёты'}}))"
exit 0
fi
exit 0
Теперь для файлов из папки Отчёты/ хук автоматически отвечает «разрешено», и Claude не прерывает работу запросом подтверждения.
Частые ошибки
Ошибка 1: Перепутать stdout и stderr
Самая распространённая ошибка. Вы хотите показать пользователю сообщение при блокировке, но пишете его в stdout, а не stderr:
# Неправильно
echo "Нельзя удалять файлы из архива"
exit 2
# Правильно
echo "Нельзя удалять файлы из архива" >&2
exit 2
При exit code 2 Claude читает stderr для показа пользователю. Если сообщение в stdout — пользователь ничего не увидит, или Claude попытается разобрать текст как JSON и получит ошибку парсинга.
Если же вы возвращаете JSON при exit code 0, он должен быть в stdout. Перепутаете — Claude не увидит ваши инструкции.
Ошибка 2: Невалидный JSON обрушивает поток
Если при exit code 0 в stdout оказывается невалидный JSON — Claude не применит инструкции. Хуже — это может нарушить работу всего шага. Всегда проверяйте, что генерируете корректный JSON:
# Проверка перед запуском
echo '{"continue": true, "systemMessage": "тест"}' | python3 -m json.tool
Если python3 выводит отформатированный JSON — всё в порядке. Если ошибку — нашли проблему.
Пустой stdout — не то же самое, что отсутствие JSON
Если скрипт завершается с exit code 0 и ничего не пишет в stdout — Claude просто продолжает. Это нормальное поведение. Как JSON разбирается только stdout, содержащий один JSON-объект и ничего кроме него — посторонний текст, например баннер из профиля шелла, ломает разбор. Поэтому весь отладочный вывод — только в stderr. Весь отладочный вывод — только в stderr.
Ошибка 3: Не учесть exit code 1 при ошибках скрипта
Если ваш скрипт падает с ошибкой (например, python3 не установлен, или опечатка в коде), он завершится с кодом 1 или другим ненулевым числом, отличным от 2. Claude не заблокируется — он просто запишет ошибку в лог и продолжит работу.
Это может создать ложное ощущение безопасности: вы думаете, что защита работает, а она тихо падает и ничего не делает. Регулярно проверяйте логи и тестируйте хуки изолированно:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf ~/Documents/Архив/file.docx"}}' | bash ~/.claude/hooks/guard_archive.sh
echo "Exit code: $?"
Если видите «Exit code: 2» — хук работает. Если «Exit code: 0» или «Exit code: 1» — что-то не так.
Ошибка 4: JSON с continue: false вместо exit code 2 — не одно и то же
Технически оба способа останавливают выполнение. Но есть разница в поведении. Exit code 2 + stderr — это сигнал «произошла намеренная блокировка», Claude отображает текст из stderr как сообщение об ошибке. JSON с continue: false + stopReason — это мягкая остановка, Claude представляет stopReason как часть своего ответа.
Для защитных хуков, где важно чёткое «запрещено», используйте exit code 2. Для логических остановок (например, «нет данных для обработки, останови работу») — JSON с continue: false выглядит естественнее.
Когда нужно / когда нет
Используйте exit code 2 (жёсткая блокировка), когда:
- Действие необратимо: удаление файлов, отправка писем, запись в финансовые документы
- Есть чёткое правило, нарушение которого недопустимо при любых обстоятельствах
- Нужно, чтобы пользователь немедленно увидел причину остановки
Используйте JSON с additionalContext, когда:
- Нужно передать Claude дополнительный контекст, зависящий от конкретного файла или действия
- Контекст меняется динамически и не подходит для статичного CLAUDE.md
- Хотите добавить предупреждение, не прерывая выполнение
Используйте JSON с permissionDecision, когда:
- Определённая папка или тип файлов всегда безопасен и не требует подтверждения
- Постоянные запросы разрешений мешают автоматическому режиму работы агента
Не добавляйте хуки с блокировкой, если:
- Правило работает «почти всегда, но с исключениями» — сложная логика в хуке быстро становится неподдерживаемой
- Вы не протестировали хук на реальных входных данных — непроверенная блокировка может остановить работу в самый неподходящий момент
- Задача решается проще через CLAUDE.md с явными инструкциями — хуки нужны для программного контроля, не для текстовых правил
Хук — это код, а не инструкция
Разница между правилом в CLAUDE.md и хуком с блокировкой принципиальная. CLAUDE.md — это инструкция, которую Claude читает и старается соблюдать. Хук — это код, который выполняется независимо от того, решил ли Claude что-то соблюдать. Для критических запретов (архив, финансы, production-данные) выбирайте хук.
Связь с другими уроками
День 41 (Hooks: события) — там мы разобрали, как настроить хук и на какое событие его навесить: PreToolUse, PostToolUse, SessionEnd. Сегодняшний урок — следующий слой: как хук общается с Claude после того, как сработал. Если день 41 — «куда подключить провод», то день 44 — «что по нему передаётся».
День 45 (Command hooks) — command hooks передают управление внешнему скрипту. Именно в этих скриптах живут exit codes и JSON-ответы, которые мы разбираем сегодня. Паттерны оттуда применяются напрямую.
День 48 (Аудит через hooks) — практический урок покажет, как использовать хуки для логирования всех действий Claude без вмешательства в их выполнение. Там exit code 0 без JSON — основной паттерн: скрипт пишет в лог и молча пропускает действие дальше.
Задание на сегодня
Создайте один защитный хук за 5 минут. Возьмите папку, которую не хотите случайно затронуть — например, папку с шаблонами или финансовыми документами.
Создайте файл ~/.claude/hooks/protect_folder.sh:
#!/usr/bin/env bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
PROTECTED="$HOME/Documents/Финансы" # замените на свой путь
if echo "$COMMAND" | grep -q "$PROTECTED"; then
echo "Заблокировано: попытка изменить защищённую папку $PROTECTED" >&2
exit 2
fi
exit 0
Сделайте его исполняемым: chmod +x ~/.claude/hooks/protect_folder.sh
Добавьте в .claude/settings.json под hooks > PreToolUse > Bash.
Проверьте: запустите в терминале вручную:
echo '{"command": "rm ~/Documents/Финансы/test.txt"}' | bash ~/.claude/hooks/protect_folder.sh; echo "Exit: $?"
Критерий выполнения: терминал показал сообщение «Заблокировано...» и «Exit: 2».
Резюме
- Exit code 0 — хук прошёл успешно; если в stdout есть JSON — Claude его читает и применяет.
- Exit code 2 — жёсткая блокировка; Claude останавливает действие и показывает пользователю текст из stderr.
- Exit code любой другой — ошибка самого хука; действие продолжается, ошибка идёт в лог.
- JSON-поля для управления поведением:
continue,stopReason,suppressOutput,systemMessage,permissionDecision. additionalContext— способ динамически добавить контекст Claude прямо в момент действия, не меняя CLAUDE.md.- Весь отладочный вывод в хуках — только в stderr; stdout зарезервирован для JSON-инструкций Claude.