Знать список событий полезно, но пока хук не лежит в файле настроек и не срабатывает, это остаётся теорией. Ценность хуков в детерминированности: форматирование после каждой правки произойдёт всегда, а не когда модель вспомнит про него.
Основная сложность первого хука — не логика, а конфигурация. Структура в settings.json состоит из трёх уровней вложенности, матчеры чувствительны к регистру, а ошибка в пути молча выключает хук: сообщения вы не увидите, просто ничего не произойдёт. Урок проводит вас через сборку рабочего хука целиком — от структуры JSON до ручной проверки скрипта в терминале.
Что уже умеем и что добавим
В прошлом уроке вы разобрали жизненный цикл сессии и узнали, какие события существуют и когда срабатывают. Теперь соберём первый работающий хук: разберём синтаксис конфигурации по уровням, научимся писать матчеры, посмотрим на входной JSON, зарегистрируем PostToolUse-хук и вынесем его логику в скрипт.
Что вы узнаете в рамках статьи
- Как устроены три уровня вложенности конфигурации хука и почему именно так
- Как писать матчеры: точные имена, регулярные выражения, события без поддержки матчера
- Какой JSON хук получает на stdin и как достать нужное поле через
jq - В какой из трёх файлов настроек класть хук и как проверить регистрацию
- Как вынести логику в скрипт
.claude/hooks/*.shи отладить его вручную
Ключевые термины
- Событие — точка жизненного цикла Claude Code, в которой запускаются хуки. Имя события служит ключом в конфигурации.
- Матчер — фильтр внутри события, ограничивающий срабатывание определёнными инструментами или типами.
- Обработчик — конкретное действие хука, описанное объектом с полем
type. Для shell-команды это"command". jq— утилита командной строки для разбора JSON. Хуки используют её, чтобы вытащить поле из входного события.${CLAUDE_PROJECT_DIR}— плейсхолдер, раскрывающийся в корень проекта сессии. Используется в путях к скриптам вместо относительных.
Три уровня вложенности конфигурации
Конфигурация хука — это объект hooks, внутри которого имена событий, внутри событий — массив групп матчера, а внутри каждой группы — массив обработчиков.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Читается это так: на событии PostToolUse есть одна группа, срабатывающая на инструментах Edit и Write, и в ней один обработчик — shell-команда. Для одного хука структура выглядит избыточной, но она нужна, чтобы на событие вешать несколько групп с разными матчерами, а в каждой группе — несколько обработчиков, запускаемых параллельно.
Каждое имя события — ключ внутри одного объекта hooks. Когда вы добавляете второе событие, оно становится соседним ключом, а не заменяет объект целиком:
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "..." }] }
],
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "..." }] }
]
}
}
И ещё: в settings.json нельзя ставить висящие запятые и комментарии — файл просто перестанет разбираться.
Как писать матчеры
Матчер фильтрует срабатывания внутри события. Правила разбора такие:
"*",""или отсутствие поля — совпадает со всем.- Строка только из букв, цифр,
_, , пробелов,,и|— список точных имён через разделитель:"Edit|Write". - Любой другой символ переводит матчер в режим регулярного выражения JavaScript без якорей:
"^Notebook","mcp__github__.*".
Матчеры чувствительны к регистру: "edit|write" не поймает ничего, потому что инструменты называются Edit и Write. Это самая частая причина «хук не работает».
Часть событий матчер не поддерживает вовсе, и значение молча игнорируется. Среди них UserPromptSubmit, Stop, PostToolBatch, MessageDisplay, TaskCreated, TaskCompleted. Хук при этом сработает, просто на всех вызовах.
Что хук получает на вход
Хук читает JSON события со stdin. Общие поля есть у всех событий: session_id, transcript_path, cwd, permission_mode, hook_event_name. У событий инструментов добавляются tool_name, tool_input, tool_use_id.
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite"
},
"tool_use_id": "toolu_01ABC123..."
}
Содержимое tool_input зависит от инструмента: у Bash там command, у Edit и Write — file_path. Достаётся оно одной строкой:
jq -r '.tool_input.file_path'
Именно эта команда стоит в начале примера с prettier: jq читает stdin, печатает путь к файлу, а xargs передаёт его форматтеру. Обратите внимание на cwd: поле следует за Claude при смене директории, тогда как ${CLAUDE_PROJECT_DIR} всегда указывает на корень проекта.
Первый рабочий хук
Разберём срабатывание примера с prettier. Claude редактирует файл через Edit. Инструмент отрабатывает успешно, наступает PostToolUse. Матчер Edit|Write совпадает, обработчик запускается, получает на stdin JSON события, вытаскивает file_path и вызывает npx prettier --write на этом файле.
В разговоре вы не увидите ничего. Результат успешного хука не показывается: при коде выхода 0 stdout уходит в отладочный лог, а не в контекст. Проверяйте хук по эффекту — файл отформатирован, строка появилась в логе.
Если нужно не форматировать, а просто вести журнал изменений, команда ещё короче:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' >> ~/.claude/edited-files.txt"
}
]
}
]
}
}
Куда класть хук и как его проверить
Файлов настроек три, и выбор между ними — выбор области действия:
| Файл | Область | Коммитится |
|---|---|---|
~/.claude/settings.json |
все ваши проекты | нет |
.claude/settings.json |
один проект | да |
.claude/settings.local.json |
один проект, личные настройки | нет |
Командные соглашения вроде форматирования кладите в .claude/settings.json, чтобы они приехали к коллегам с репозиторием. Личные привычки — в пользовательский файл, эксперименты — в settings.local.json.
Правки подхватываются файловым наблюдателем; если через несколько секунд ничего не изменилось, перезапустите сессию. Команда /hooks показывает настроенные хуки по событиям: матчер, тип, файл-источник и команду. Меню только для чтения — менять хуки нужно правкой JSON.
Скрипт удобно проверять до подключения, прямо в терминале:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?
Так вы убедитесь, что логика работает и код выхода тот, который вы ожидали. Если хук зарегистрирован, но не срабатывает, помогает отладочный лог:
claude --debug-file /tmp/claude.log
tail -f /tmp/claude.log
Вынесение логики в скрипт
Однострочник в JSON быстро упирается в экранирование кавычек. Как только логике нужны условия, выносите её в файл .claude/hooks/:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -n "$FILE_PATH" ]]; then
echo "$(date -u +%FT%TZ) $FILE_PATH" >> "$CLAUDE_PROJECT_DIR/.claude/edits.log"
fi
exit 0
Скрипт обязательно нужно сделать исполняемым, иначе хук молча не запустится:
chmod +x .claude/hooks/log-edits.sh
Регистрация выглядит так, путь здесь абсолютный:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-edits.sh"
}
]
}
]
}
}
Относительный путь вроде ./.claude/hooks/log-edits.sh ненадёжен: рабочая директория меняется, и вы получите command not found. ${CLAUDE_PROJECT_DIR} остаётся на корне проекта.
Частые ошибки
- Неверный регистр в матчере.
"edit|write"не совпадёт ни с чем: инструменты называютсяEditиWrite. Хук виден в/hooksи при этом никогда не срабатывает. - Пропущенный уровень вложенности. Соблазнительно написать
"PostToolUse": { "type": "command", ... }, минуя группу матчера. Уровней ровно три: событие, массив групп, массив обработчиков. - Второе событие заменяет первое. Добавляя
PreToolUse, положите его соседним ключом внутри существующего объектаhooks, а не вторым ключомhooks. - Относительный путь к скрипту или забытый
chmod +x. И то и другое выключает хук без сообщения об ошибке. Используйте${CLAUDE_PROJECT_DIR}и проверяйте скрипт пайпом.
Наиболее частые вопросы
- Почему я не вижу вывод своего хука в разговоре? — Результат успешного хука не показывается. Смотрите на эффект или запускайте
claude --debug-file /tmp/claude.log. - Можно ли добавить хук через меню
/hooks? — Нет, меню только для чтения. Оно показывает событие, матчер, тип, источник и команду, но править надо JSON. - Что будет, если матчер указан на событии, которое его не поддерживает? — Значение молча игнорируется, хук сработает на всех случаях этого события.
- Нужно ли перезапускать сессию после правки настроек? — Обычно нет, изменения подхватываются файловым наблюдателем. Если эффекта нет, перезапустите.
- Почему
jq: command not found? — Утилита не установлена. Хуки из примеров опираются наjq, поставьте её пакетным менеджером системы.
Итоги
У вас теперь есть полная картина сборки хука: вы понимаете, зачем в конфигурации три уровня вложенности, умеете писать матчеры и знаете, что регистр в них важен, читаете входной JSON и достаёте поля через jq, выбираете файл настроек под нужную область и проверяете скрипт пайпом до подключения к сессии. Это уже рабочий навык: PostToolUse с логированием или форматированием — самый частый первый хук в реальных проектах.
Пока ваши хуки только наблюдают за происходящим. В следующем уроке мы перейдём к хукам, которые вмешиваются в ход работы: блокируют опасные вызовы, возвращают решение и закрепляют политику, которую нельзя обойти сменой режима разрешений.