JavaRush /Курсы /Claude code /Hook debugging, side effects и recovery

Hook debugging, side effects и recovery

Claude code
14 уровень , 4 лекция
Открыта

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 — тот, которому вы доверяете, потому что понимаете, как он срабатывает, как его диагностировать и как выключить без театра и потери контроля.

1
Задача
Claude code, 14 уровень, 4 лекция
Недоступна
Quick disable misbehaving hook
Quick disable misbehaving hook
1
Задача
Claude code, 14 уровень, 4 лекция
Недоступна
Сужение matcher у noisy hook
Сужение matcher у noisy hook
1
Опрос
Hooks в Claude Code, 14 уровень, 4 лекция
Недоступен
Hooks в Claude Code
Hooks в Claude Code
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ