1. Признаки того, что hook начинает мешать
Пока hook работает тихо и по делу, его легко воспринимать как полезный фон: что-то автоматически отформатировалось, где-то заблокировалась опасная команда, и всем хорошо. Проблемы начинаются в тот момент, когда правка одного файла тянет три лишних запуска, корректная команда блокируется, а сессия тормозит так, будто вы запустили видеомонтаж на калькуляторе.
Самая частая ошибка в такой момент — сразу бросаться в скрипт и править его наугад. С hooks это почти не помогает: проблема может жить не в handler, а на любом предыдущем шаге. Debugging сводится к четырём вопросам: hook загружен, событие произошло, matcher совпал, handler отработал как ожидалось? И пятый, самый коварный: даже если всё формально сработало, не создал ли hook побочный эффект, из-за которого стало хуже?
Держите в голове схему:
flowchart TD
A[Hook ведёт себя странно] --> B{Hook зарегистрирован?}
B -- нет --> C[Проверить scope, файл конфига, reload]
B -- да --> D{Событие и matcher совпали?}
D -- нет --> E[Проверить event, paths, tool, regex]
D -- да --> F{Handler завершился нормально?}
F -- нет --> G[Смотреть exit code, stderr, quoting, права, timeout]
F -- да --> H[Искать side effects: шум, loop, лишние edits, slow workflow]
H --> I[Quick disable → staged rollback → аккуратный фикс]
Схема не даёт спорить с автоматизацией в стиле «ну ты же вчера работал». Вчера работал — прекрасно, сегодня проверяем путь по шагам. Hook — не заклинание, а конфиг плюс событие плюс фильтр плюс команда, и чинится обычной инженерной логикой.
2. Первая остановка: hook вообще зарегистрирован?
Когда automation ведёт себя странно, очень хочется сразу открыть скрипт и искать в нём драму. Обычно это преждевременно. Сначала подтвердите базовый факт: hook загружен, активен и пришёл из того scope, откуда вы его ждёте. Иначе полчаса чините handler, которого в сессии нет.
Для этого нужен hook manager — обычно /hooks или эквивалент. Точные имена подкоманд меняются, поэтому привычка та же, что и в других темах курса: знаете слой, а актуальный синтаксис проверяете через /help и встроенную справку.
/hooks # показывает активные hooks в текущей сессии
/hooks status format-on-edit # показывает источник, matcher и последний статус hook
/hooks reload # перечитывает конфигурацию, если ваша версия это поддерживает
Если hook не виден в списке, проблема часто оказывается совсем прозаичной: конфиг лежит не там, где система его ждёт; конфигурация не перечиталась после изменения; в JSON синтаксическая ошибка. Кажется, что «ничего не работает», а система просто аккуратно проигнорировала битый конфиг.
Отдельно полезно смотреть на scope. В Workflow Kit hook живёт и в проектной конфигурации, и в личном окружении разработчика. Два похожих hook на одно событие — и вот вам «почему formatter запускается два раза».
Хорошее правило здесь звучит так: пока вы не убедились, что hook зарегистрирован и его вызывает нужное событие, не лечите handler. Иначе это как менять колёса, не проверив, завели ли вы вообще свою машину.
3. Handler — это обычная команда: читайте exit code и вывод
Когда вы подтвердили, что hook загружен и matcher действительно срабатывает, следующим подозреваемым становится handler. С точки зрения системы это не «умный кусок hook-магии», а обычная команда, и читается как любая другая: по коду завершения и по выводу.
Практически это значит следующее. Нулевой exit code обычно означает успех. Ненулевой — либо handler упал, либо hook сознательно сигнализирует, что действие нужно заблокировать: 0 — хорошо, остальное требует внимания.
Не менее важны stdout и stderr. Сыпете туда всё подряд — получаете шумный hook, который загрязняет лог, а иногда и контекст сессии. Штатные сообщения держите минимальными, ошибки и диагностику отправляйте в stderr.
Хороший минималистичный handler обычно выглядит примерно так:
#!/usr/bin/env bash
set -euo pipefail
FILE="$1"
case "$FILE" in
*.generated.*) exit 0 ;; # generated-файлы пропускаем
esac
prettier --write "$FILE" >&2 || { echo "format failed: $FILE" >&2; exit 1; }
exit 0
Здесь есть несколько важных мелочей. set -euo pipefail не даёт проглатывать ошибки молча. "$FILE" в кавычках, чтобы путь не развалился. Ранний exit 0 для файлов, которые не нужно трогать. Понятное сообщение об ошибке в stderr, если форматирование не удалось.
Обратите внимание: большинство «таинственных» hook-багов оказываются не логическими, а механическими. Скрипт ломается внутри hook из-за quoting. Или падает, но вы этого не видите: всё печатается в stdout. Или ловит исключение, бодро что-то сообщает и завершается кодом 0 — hook выглядит исправным, хотя не сделал ничего полезного.
Если хотите быстро понять, кто виноват, запустите handler вручную с тем же аргументом, что он получает от hook. Половина мистики исчезает сразу.
4. Самые частые места поломки hooks
Когда начинаешь работать с hooks чуть дольше, быстро замечаешь: сбои у них довольно повторяемы. У большинства узнаваемые паттерны — научитесь узнавать их по симптомам, и debugging станет заметно спокойнее.
Ниже — короткая карта, с которой удобно сверяться, когда hook ведёт себя странно:
| Симптом | Что вероятнее всего происходит | С чего начать |
|---|---|---|
| Hook вообще не срабатывает | Не загрузился конфиг, неверный event, matcher не совпадает | Проверить /hooks, перечитать конфиг, посмотреть JSON |
| Hook срабатывает слишком часто | Слишком широкий matcher по paths или tool | Сузить glob/regex, убрать лишние директории |
| Hook блокирует корректное действие | Blocking-логика слишком широкая, handler возвращает ненулевой код | Временно перевести в non-blocking/log-only и проверить вход |
| Hook меняет неожиданные файлы | Handler форматирует не конкретный файл, а широкий каталог | Ограничить работу одним файлом, добавить guard |
| Ошибка есть, но причина непонятна | Shell quoting, права на запуск, timeout, битый JSON | Запустить handler вручную, проверить stderr, executable bit |
| После правки всё работает, но медленно | Слишком тяжёлая команда на каждом edit | Перенести проверку в reporting-слой или сузить matcher |
Здесь особенно коварны три вещи. Первая — невалидный JSON. Одна лишняя запятая, и система может просто не загрузить hook, не устраивая при этом драматического спектакля. Вторая — права на запуск. Если скрипт не executable, можно долго спорить с matcher, хотя проблема тривиальна. Третья — timeouts. Hook может быть логически правильным, но если он запускает тяжёлую команду на каждом edit, его практическая польза быстро уходит в минус.
Отдельно хочу подчеркнуть shell quoting. Ошибка в духе prettier --write $FILE вместо prettier --write "$FILE" выглядит мелочью, но именно из таких мелочей часто рождаются самые нелепые баги. Человек потом честно говорит: «Но ведь formatter же запускался». Да, запускался. Просто путь до файла система прочитала не так, как вы ожидали.
С hooks вообще полезно помнить простую вещь: если поведение выглядит слишком странным, сначала проверьте банальные механические причины. Они скучные, зато очень часто попадают в точку.
5. Побочные эффекты: hook работает, но жизнь хуже
Есть особый класс hook-проблем, который раздражает сильнее всего. Всё вроде бы корректно: hook загружается, matcher совпадает, handler выполняется, ошибок нет. Формально можно поставить галочку «работает». Практически хочется выключить всё к чертям, потому что после этого «исправного» hook жить стало заметно менее приятно.
Самый популярный пример — шум. Hook на каждый edit сообщает столько диагностической информации, что вы перестаёте обращать внимание на его сообщения вообще. Это плохо не только психологически. Избыточный вывод может загрязнять рабочий контекст сессии и мешать самому Claude разбирать, что сейчас важно, а что просто фон. То есть hook может быть технически полезным, но операционно вредным.
Второй класс побочек — лишние изменения файлов. Например, вы думали, что hook аккуратно форматирует только текущий файл, а он каждый раз запускает команду по целой директории. Разработчик редактировал один маленький service.ts, а получил diff на пятнадцать файлов. С таким hook код вроде и «красивый», но review превращается в квест.
Третий класс — loops. Классика жанра: after-edit hook меняет файл, это изменение снова считается edit, hook запускается ещё раз — и так по кругу. Иногда цикл виден сразу, иногда проявляется как «почему у меня всё так странно подтормаживает». От loops обычно спасают два уровня защиты: узкий matcher и ранний guard в самом handler.
Вот типичный до-фикса вариант того же format-on-edit, когда matcher расползся слишком широко:
{
"event": "afterFileEdit",
"matcher": { "paths": ["frontend/src/**/*"] },
"handler": { "command": "scripts/format-on-edit.sh {{file}}" }
}
Такой вариант слишком щедрый. Он цепляет вообще всё во frontend/src, включая то, что форматировать не надо. Фикс — вернуть matcher к рабочему baseline и снова ограничить его только ts/tsx-файлами фронтенда:
{
"event": "afterFileEdit",
"matcher": { "paths": ["frontend/src/**/*.{ts,tsx}"] },
"handler": { "command": "scripts/format-on-edit.sh {{file}}" }
}
Файлы стало проще предсказать, а значит, и поведению hook снова можно доверять. А доверие в automation — это половина пользы. Никому не нужен «умный помощник», который с энтузиазмом улучшает не ту часть проекта.
Есть ещё две неприятные побочки, о которых часто вспоминают поздно. Первая — скрытие исходной ошибки. Бывает, что основная команда уже упала, но hook перехватывает ситуацию так, что в логах видно только его собственное сообщение. Вторая — утечка чувствительных данных в лог. Если handler бездумно печатает содержимое переменных окружения, пути или куски файлов, вы сами создаёте себе ненужный риск. Поэтому хороший hook — это не только корректный hook, но и аккуратный hook.
6. Recovery path: отключить, откатить, зафиксировать
Очень легко влюбиться в автоматизацию в тот момент, когда она работает. Но зрелость automation проявляется не тогда, когда всё красиво, а тогда, когда вы можете одним движением вернуть себе управляемость. Правило здесь жёсткое и очень полезное: automation without a recovery path is not ready. И это не красивая фраза, а практический критерий качества.
Если hook уже мешает работать, первая цель — не «починить архитектурно», а быстро снять нагрузку. Способ зависит от того, как именно hooks устроены в вашей версии Claude Code и в проекте. Где-то есть удобное точечное отключение через встроенный интерфейс управления, где-то используется локальный override, а где-то на вашей ветке временно переименовывают конфиг или убирают его из активной папки. Принцип всегда один: сначала быстро вернуть себе рабочую сессию, потом разбираться красиво.
После quick disable начинается staged rollback. Это уже не аварийная реакция, а нормальная инженерная работа. Если проблема была в общем командном hook внутри Workflow Kit, фикс должен вернуться в репозиторий и пройти как обычное изменение: с понятным diff, осмысленным коммитом и коротким объяснением причины. Hook — это такой же артефакт команды, как SKILL.md или CLAUDE.md. Его нельзя просто молча «отключить у себя и забыть».
Хорошая короткая запись в истории изменений может выглядеть так:
## 2026-05-24
- narrowed `format-on-edit` matcher to `src/**/*.{ts,tsx}`
- excluded `*.generated.*` in handler
- reason: hook retriggered on generated files and slowed edits
Это кажется мелочью, но на практике такая заметка экономит команде массу времени. Через месяц никто не будет помнить, почему matcher стал уже и зачем в скрипте появился guard. CHANGELOG.md или короткая заметка рядом с hook возвращают контекст без археологии.
Здесь полезно мыслить очень приземлённо. Автоматизация без кнопки stop — это как робот-пылесос, который мило ездит до первого столкновения с миской кота. Пока остановка не предусмотрена, устройство кажется умным. После первого сбоя оказывается, что у вас не помощник, а маленькая автономная проблема. Поэтому у каждого hook должен быть понятный путь: как его отключить, как откатить и как объяснить фиксацию остальным.
7. Лечим format-on-edit в Workflow Kit
Теперь давайте соберём всё в одну нормальную рабочую историю. Представьте, что в Workflow Kit у вашей команды уже есть базовый .claude/hooks/format-on-edit.json: узкий matcher по frontend/src/**/*.{ts,tsx} и форматирование только для изменённого файла. Первые дни все довольны. Потом рядом с обычным кодом начинают чаще появляться generated-файлы, matcher в одном из изменений размывают до frontend/src/**/*, и внезапно каждый edit сопровождается лишними переписываниями, а иногда и повторными срабатываниями.
Первое, что вы делаете, — не открываете Prettier и не идёте читать Stack Overflow в позе страдания. Вы заходите в hook manager и подтверждаете, что hook активен, загружается из project scope и действительно вешается на after-edit событие. После этого воспроизводите проблему на конкретном файле. Оказывается, hook срабатывает и на refund.generated.ts. Значит, конфиг жив, событие совпало — смотрим дальше.
Следующий шаг — handler. Вы запускаете его вручную с тем же файлом и видите, что скрипт сам по себе не падает. Форматирование отрабатывает нормально. Это важный момент: проблема не в «битом formatter», а в том, что hook срабатывает слишком широко. Дальше вы проверяете matcher и находите старый glob frontend/src/**/*. Вот и ответ: hook добросовестно делает то, что ему приказали. Базовый format-on-edit не «сломался магически» — его просто незаметно распахнули шире, чем нужно.
На этом этапе полезно временно снять боль: локально отключить hook у себя, чтобы вернуть нормальную скорость работы, а уже потом внести центральный фикс. После этого вы сужаете matcher обратно до frontend/src/**/*.{ts,tsx} и добавляете в handler ранний выход для *.generated.*. Проверяете ещё раз на обычном файле и на generated-файле. На обычном всё форматируется. На generated-файле handler молча выходит с 0, ничего не ломая и не шумя.
После этого фикс идёт в Workflow Kit как обычное изменение: конфиг, при необходимости scripts/format-on-edit.sh, короткая запись в CHANGELOG.md и пара строк в hooks/README.md, чтобы следующий человек не удивлялся, почему generated-файлы намеренно исключены. В итоге hook не стал «умнее» в романтическом смысле. Он стал лучше в инженерном: предсказуемее, тише и безопаснее.
И вот это, пожалуй, самое важное отношение к hooks. Хороший hook — не тот, который делает больше всех действий. Хороший hook — тот, которому вы доверяете, потому что понимаете, как он срабатывает, как его диагностировать и как выключить без театра и потери контроля.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ