Hooks: подієва автоматика
На минулому вебінарі Claude Code отримав доступ у зовнішній світ через MCP. Сьогодні спершу закриємо хвіст цієї теми - маршрут зовнішнього сигналу. А потім головне: hooks - автоматика всередині самого Claude Code. Вона сама форматує код, блокує небезпечну команду, веде журнал - і спрацьовує незалежно від рішення моделі.
Сьогодні розберемо:
- маршрут "зовнішній сигнал -> гіпотеза -> перевірка" - від тикета й alert до артефакта в репозиторії;
- що таке hook: подія, умова, дія - на реальному форматі settings.json;
- запобіжники, сходи розгортання і хуки життя сесії;
- налагодження мовчазного хука за п'ять питань і шлях назад.
Сигнал ззовні - робота всередині
Сервер підключений, тикет читається прямо із сесії. І перша спокуса: "Claude, зроби, як написано в тикеті".
Проблема в тому, що коментар у тикеті - чиясь здогадка, кинута на ходу. "Мабуть, проблема в кеші" - це версія, а не діагноз.
Тому в зовнішнього сигналу один маршрут - через гіпотезу й перевірку:
Середина маршруту - захист від моменту, коли сервіс підкинув гору context, а ви прийняли її за готовий діагноз. Жоден tool не дає права перестрибнути крок "зрозуміти, що відбувається в коді".
Правило просте: tool скорочує шлях до перевірки, але не замінює її. Перевірка замикається на коді й тестах - не на впевненості зовнішнього джерела. І знайома рамка з минулого вебінару працює і тут: текст тикета - дані, а не інструкції. Просимо витягти симптоми й кроки, а не виконати чужу гіпотезу.
Коли такий маршрут повторюється із сесії в сесію, його рутинні кроки час віддати автоматизації. Ось тут починаються hooks.
Що таке hook
Порахуйте, скільки разів за тиждень ви повторили одне й те саме: прогнати formatter після правки, перевірити, чи не поліз Claude у .env, нагадати собі на початку сесії "перевір git status". Щоразу - вручну або "Claude, не забудь".
Hook - автоматика, яка спрацьовує сама, коли всередині Claude Code відбувається подія:
- правка файлу;
- запуск команди;
- старт сесії.
MCP - двері назовні, hook - автоматика всередині дому. Як світло з датчиком руху в під'їзді: вимикачем ніхто не клацає - датчик ловить кроки і сам вмикає лампу, і тільки коли темно. Кроки - подія, "коли темно" - умова, увімкнути світло - дія.
Hook спить, доки подія не сталася. Він не думає, не планує і не досліджує код: збіглася умова - виконав одну команду. Це його сила і його межа.
І головна відмінність від усього, що ви налаштовували раніше: інструкцію в CLAUDE.md модель може забути або проігнорувати. Hook спрацьовує незалежно від її рішення - це не побажання моделі, а автоматика поверх неї.
Перший hook за хвилину
Біль конкретний: після кожної правки Claude ви запускаєте formatter вручну - або забуваєте, і в diff їдуть зайві пробіли. Хук живе у вже знайомому файлі - .claude/settings.json проєкту:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Читаємо як речення: після інструмента правки файлів - виконай команду. jq - утиліта, що дістає поля з JSON: тут вона бере шлях зміненого файлу й віддає його prettier. Якщо jq у системі немає - встановіть: його відсутність - часта причина мовчазних хуків.
Ви: виправ друкарську помилку в src/report.ts
Claude: Edit src/report.ts -> файл змінено
PostToolUse hook -> prettier --write src/report.ts
У diff - тільки ваша правка. Форматування пішло у фон назавжди.
Перевірити, що hook завантажений: /hooks - браузер усіх хуків за подіями. Меню тільки для перегляду; правки - у settings.json, файл перечитується на льоту.
Контракт із трьох частин
Хук перестає бути магією, коли ви читаєте його як контракт із трьох частин:
| Частина | Питання | У конфігу вище |
|---|---|---|
| подія | коли реагувати? | PostToolUse: після успішного виклику інструмента |
| matcher | за якої умови? | Edit|Write: тільки інструменти правки файлів, не кожен Bash |
| команда | що зробити? | "type": "command" + рядок команди: prettier по конкретному файлу |
Хуки живуть у тих самих settings-файлах, і scope працює так само.
~/.claude/settings.json- ваші хуки;.claude/settings.json- хуки проєкту;.claude/settings.local.json- особисті хуки цього проєкту, не для команди.
Тобто hook - командний артефакт: чужий блокувальний хук видно в diff до того, як він почне заважати.
Карта подій: три групи
Подій кілька десятків, заучувати список не треба. Тримайте карту з трьох груп - майже всі корисні хуки потрапляють в одну з них:
| Група | Події | Навіщо |
|---|---|---|
| до дії | PreToolUse | єдина точка, де дію можна зупинити до виконання: захист шляхів, блок небезпечних команд |
| після дії | PostToolUse | реакція на те, що вже сталося: формат, швидка перевірка, запис у лог |
| життя сесії | SessionStart, UserPromptSubmit, Stop, Notification | старт сесії, ваш prompt, Claude закінчив відповідати, Claude чекає на вас |
За назвами все чесно: Pre - до, Post - після, решта - моменти життя сесії. Перша група - запобіжник, друга - реакція, третя - дисципліна й пам'ять. Повний список подій завжди видно в /hooks - там само, де ви перевіряли свій перший хук.
"type": "prompt" і "type": "agent" - рішення приймає не скрипт, а модель. Це просунутий шар: спершу навчіться жити зі звичайними командними.Хук відповідає exit-кодом
Хук і Claude Code розмовляють найстарішою мовою термінала. На вхід скрипт отримує JSON події через stdin:
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" },
"cwd": "/home/dev/project"
}
На вихід - код завершення. Нагадування в одну фразу: exit code - число, яким завершується будь-яка команда, 0 - успіх.
- exit 0 - хук не заперечує, дія йде далі своїм шляхом;
- exit 2 - блок: дія скасовується, а все, що скрипт написав у stderr - потік помилок, - іде Claude як пояснення, і він перебудовується;
- будь-який інший код - помилка самого хука: покажеться в transcript, але нічого не заблокує.
Запам'ятайте точно: блокує саме 2. Не "будь-який ненульовий" - саме 2. Це часте джерело хуків, які "ніби блокують, але ні".
Matcher: вузько краще, ніж широко
Найшумніші хуки народжуються не з поганих команд, а з широких matcher: "на всі інструменти" сипле повідомленнями на кожну дію, доки їх не перестають помічати. Далі хук можна не вимикати - його вже ніхто не чує.
Для подій-інструментів matcher - ім'я інструмента, можна кілька і можна regex:
Edit|Write- тільки правки файлів;Bash- тільки команди термінала;mcp__github__.*- усі інструменти MCP-сервера github: хуки бачать і їх, дві половини вебінару щойно зустрілися;- порожній рядок - на кожне спрацювання події. Іноді потрібно, частіше - шум.
Matcher чутливий до регістру: edit не збігнеться з Edit - друга за популярністю причина "хук мовчить". У подій життя сесії matcher свій: SessionStart матчиться по тому, як сесія почалася - startup, resume, clear, compact, а Notification - за типом повідомлення.
Правило: matcher вузький настільки, наскільки розумно. Нудний передбачуваний хук - хороший хук.
Запобіжник: блокуємо rm
Claude виконує Bash-команди, а в rm немає кнопки undo. Файл, над яким ви сиділи день, видаляється за секунду - і git врятує, тільки якщо ви встигли закомітити. Запобіжник - PreToolUse-хук на Bash, скрипт .claude/hooks/block-rm.sh:
#!/bin/bash
INPUT=$(cat) # JSON події зі stdin
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qw "rm"; then
echo "Blocked: видалення файлів - тільки вручну" >&2 # пояснення для Claude
exit 2 # 2 = заблокувати
fi
exit 0
Реєстрація в .claude/settings.json: подія PreToolUse, matcher Bash, команда - шлях до скрипта. І живий прогін:
Ви: видали стару чернетку notes-old.md
Claude: Bash rm notes-old.md
PreToolUse hook -> exit 2: "Blocked: видалення файлів - тільки вручну"
Claude: команду заблокував hook проєкту. Видаліть файл самі,
якщо він точно не потрібен.
Зверніть увагу: Claude не вперся і не шукає обхідні шляхи - він отримав причину через stderr і передав рішення вам. І один рядок про силу цього механізму: блокування хуком працює навіть у bypassPermissions - це політика проєкту, яку не обійти зміною режиму.
Чесна рамка наостанок: це навчальний guardrail, а не гарантія. Скрипт ловить буквальне rm у команді - а видалити файл можна й іншими способами, яких він не бачить. Жорстка заборона - це deny-правило в permissions; хук - страховка від найчастішого випадку.
Хуки життя сесії
Третю групу подій зазвичай недооцінюють: вона нічого не блокує і не форматує. Зате робить сесії дисциплінованими - і залишає пам'ять про роботу.
SessionStart - особлива подія: усе, що скрипт надрукує в stdout при exit 0, потрапляє прямо в context сесії. Таких подій дві - друга UserPromptSubmit. Банер "на чому зупинилися":
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "git log --oneline -5" }
]
}
]
}
}
Claude починає сесію, уже бачачи останні п'ять комітів - не треба переказувати, де ви зупинилися.
Далі журнал і сигнал:
UserPromptSubmit- журнал сесії: кожен ваш prompt дописується у файл у репозиторії -jq -r '.prompt' >> "$CLAUDE_PROJECT_DIR"/.claude/session-log.md. Через тиждень видно, що й чому ви робили;Stop- Claude закінчив відповідати: довге завдання працює, ви займаєтеся своїми справами, а сигнал або звук повертає вас до термінала;Notification- окрема подія "Claude чекає на вас": підтвердження прав або простій. Різниця зі Stop - "закінчив" проти "чекає вашого рішення".
Ці три сценарії - банер, журнал, сигнал - рівно те, що ви зберете в практиці.
Сходи розгортання
Blocking-хук із широкою умовою - найшвидший спосіб зненавидіти власну автоматизацію: він зупинить і небезпечне, і нормальне, і ви сперечатиметеся з власним шлагбаумом. Тому хук дорослішає сходами, а не народжується блокувальним:
На першій сходинці хук нічого не змінює - тільки пише, що спрацював би. Нудно, але безцінно: одразу видно, чи не занадто широкий matcher. На другій - робить безпечну дію. І тільки на третій отримує право сказати "стоп", причому на вузький випадок на кшталт rm.
- кожна сходинка живе хоча б пару робочих сесій - хук ловить зайве на log-only, і це ідеальний момент чинити matcher, поки він нікому не заважає;
- командний хук у project settings - окрема верхня сходинка: туди підіймаються хуки, обкатані особисто й описані в README;
- чесне калібрування: суворі сходи обов'язкові там, де хук уміє заважати, - blocking і все, що йде команді. Особистий банер можна ввімкнути одразу: ціна помилки - зайвий рядок на старті сесії.
Хук мовчить: п'ять питань
Хуки ламаються мовчки: немає червоного банера "hook failed, вивчіть проблему". Просто перестав форматуватися код - і все. Тому налагодження - не інтуїція, а п'ять питань по черзі:
- чи завантажений? -
/hooks: чи є він у списку. Часта причина - битий JSON: одна зайва кома, і конфіг мовчки ігнорується; - чи та подія? - PreToolUse і PostToolUse легко переплутати: хук чекає "до", а ви дивитеся "після";
- чи збігся matcher? - регістр букв,
Bashнеbash; правили через Edit, а ловите Write; - чи відпрацював скрипт? - перевіряється вручну за 10 секунд;
- чи немає side effects? - формально працює, а жити заважає: шум у transcript, diff на п'ятнадцять файлів замість одного, секрети в журналі. Побічку не видно в exit-коді - її видно з того, що працювати стало важче, ніж до автоматизації.
Ручний прогін для четвертого питання - подаєте скрипту той самий JSON, що подав би Claude Code:
echo '{"tool_name":"Bash","tool_input":{"command":"rm x.md"}}' | ./block-rm.sh
echo $? # очікуємо 2; отримали 0 - проблема всередині скрипта
Найчастіше винна механіка, а не логіка: скрипт не executable - потрібен chmod +x, шлях без лапок розвалився на пробілі, jq не встановлений - а на ньому тримається ваш перший хук.
Де дивитися: Ctrl+O - transcript із рядком по кожному спрацьованому хуку; /debug - повний лог із matcher, exit-кодами і виводом.
/hooks і transcript - інтерфейс для людини. Налаштування перечитуються на льоту, а /hooks потрібен, щоб перевірити, що конфіг завантажився саме так, як ви очікували.Recovery: вимкнути, відкотити, зафіксувати
Зрілість автоматизації перевіряється не тим, як вона працює, а тим, як швидко ви можете її зупинити. Автоматизація без шляху назад не готова до життя. Один випадок Claude Code страхує сам: Stop-хук, який щоразу відповідає "продовжуй працювати", після 8 блокувань поспіль перекривається - але решту побічок вимикати вам.
Швидко зняти навантаження, коли хук заважає прямо зараз:
- прибрати його блок із settings.json - файл перечитується на льоту, перезапуск не потрібен;
- або рубильник цілком:
"disableAllHooks": trueу settings - і всі хуки замовкають, доки розбираєтеся.
Повернули собі робочу сесію - тепер спокійний фікс: відкат хука на сходинку нижче, звужений matcher, guard у скрипті. І короткий запис поруч із хуком - чому звузили: за місяць ніхто не згадає, навіщо в скрипті виняток для generated-файлів.
Автоматизація без кнопки stop - як розумна колонка, яка не розуміє слово "досить": доки не висмикнете з розетки, це не помічник, а сусід.
Практика: завдання і мета
Тепер ваш хід. На минулому вебінарі ви будували Claude доступи назовні; тепер будуєте автоматику всередині. Інструмент цієї практики - обв'язка проєкту: набір хуків, які реагують на події і додають проєкту зручність, безпеку та пам'ять.
Завдання: іграшковий репозиторій - маленький передбачуваний проєкт зі стартового комплекту, де є що правити, видаляти й логувати, - плюс .claude/settings.json із хуками та скрипти-обробники. Мета: прожити всі три групи подій і розгорнути хоча б один хук сходами, а не одразу blocking.
Фокус суворо на хуках: жодного докера і БД в основній частині - інфраструктура не має з'їсти час, відведений на автоматику. Мультиагентність і pipeline сюди теж не тягнемо - це теми наступних вебінарів. І кожен хук відповідає на питання "навіщо це в житті" - зручність, безпека або пам'ять, а не "ось механіка події".
До кінця практики в репозиторії мають лежати:
.claude/settings.jsonіз хуками - у git;- скрипти-обробники поруч, у
.claude/hooks/; - журнал сесії
.claude/session-log.md- у репозиторії; - README: який хук на яку подію, що робить і навіщо, чому журнал у repo, як розгортали, як лагодили - і бонусні хуки, якщо робили;
- демо-прогони в
log/.
Практика: три хуки на три групи
Три базові хуки - по одному на групу подій, і в кожного життєва користь, а не демонстрація механіки:
| Група | Хук | Подія | Навіщо в житті |
|---|---|---|---|
| після | Сигнал завершення - звук або повідомлення, коли Claude закінчив | Stop | ШІ працює довго: займайтеся своїми справами, повертайтеся за сигналом |
| до | запобіжник на rm - блок через exit 2 | PreToolUse + matcher Bash | захист від незворотного: не дасть знести файл, над яким сиділи |
| сесія | журнал у repo: prompts і ключові рішення | UserPromptSubmit | пам'ять і звітність: через тиждень видно, що й чому наробили |
Технічні застереження - чесні обмеження, вони теж частина інженерії:
- окремого інструмента "видалити файл" немає: видалення - це
Bash rm, тому before-хук ловить команду зі словом rm. Старт із буквальногоgrep -qw "rm", ускладнення - сходами; це guardrail від найчастішого випадку, а не гарантія; - журнал живе в репозиторії: transcript у
~/.claudeне комітиться і лишається тільки на вашій машині. Журнал - самодостатній артефакт, який подорожує разом із проєктом; - prompt ловить UserPromptSubmit - відповіді в цей момент ще немає; відповідь ловить Stop: у його вхідному JSON є поле
last_assistant_messageз текстом останньої відповіді. У журнал кладемо перші ~100 символів - заради читабельності, а не тому що більше не можна; - звук:
sayна macOS; на Windows і Linux чесний fallback - запис "готово" в лог; - не логуйте секрети: журнальний хук не пише env-змінні, токени і шматки
.env.
Практика: демо-прогони і підсумок
Підсумок практики - не JSON, а докази, що автоматика жила. Чотири демо-прогони в log/:
- after: Claude закінчив завдання - спрацював сигнал, у логу позначка;
- before: спроба
rm- хук заблокував, у логу причина блоку; - session: журнал наповнився за робочу сесію - prompts і рішення на місці;
- debugging: один хук мовчав - у README розбір за п'ятьма питаннями, як знайшли причину.
Зафіксуйте сходи в README: як before-хук пройшов log-only -> non-blocking -> вузький blocking, а не народився блокувальним.
Чек-лист самоперевірки:
.claude/settings.jsonіз хуками в git;- три базові хуки працюють - по одному на групу, доведено логами;
- before-хук блокує rm через exit 2 і розгорнутий сходами;
- журнал у repo, самодостатній, без секретів;
- README: що / навіщо / як розгортали / як лагодили;
- розбір мовчазного хука за п'ятьма питаннями.
Бонуси для швидких - за бажанням: захист БД від DROP і DELETE - підняти postgres у докері силами Claude Code, before-хук на Bash/psql-команду блокує небезпечний виклик; банер SessionStart "на чому зупинилися"; будь-який свій - захист файлів за маскою, автоформат після правки.