JavaRush /Курси /Claude code /Hook debugging, побічні ефекти та відновлення

Hook debugging, побічні ефекти та відновлення

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, повільний 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 "форматування не вдалося: $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
- звузили `format-on-edit` matcher до `src/**/*.{ts,tsx}`
- виключили `*.generated.*` у handler
- причина: hook повторно запускався на generated-файлах і сповільнював редагування

Це здається дрібницею, але на практиці така нотатка економить команді масу часу. Через місяць ніхто не памʼятатиме, чому matcher став вужчим і навіщо в скрипті зʼявився guard. CHANGELOG.md або коротка нотатка поруч із hook повертають контекст без археології.

Тут корисно мислити дуже приземлено. Автоматизація без кнопки зупинки — це як робот-пилосос, який мило їздить до першого зіткнення з мискою кота. Поки зупинку не передбачено, пристрій здається розумним. Після першого збою виявляється, що у вас не помічник, а маленька автономна проблема. Тому в кожного 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
Опитування
Hooks у Claude Code, рівень 14, лекція 4
Недоступний
Hooks у Claude Code
Hooks у Claude Code
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ