Hooks: подієва автоматика

На минулому вебінарі Claude Code отримав доступ у зовнішній світ через MCP. Сьогодні спершу закриємо хвіст цієї теми - маршрут зовнішнього сигналу. А потім головне: hooks - автоматика всередині самого Claude Code. Вона сама форматує код, блокує небезпечну команду, веде журнал - і спрацьовує незалежно від рішення моделі.

Сьогодні розберемо:

Сьогодні пройдемо: рівень 14. Hooks і tool integration workflows.

Сигнал ззовні - робота всередині

Сервер підключений, тикет читається прямо із сесії. І перша спокуса: "Claude, зроби, як написано в тикеті".

Проблема в тому, що коментар у тикеті - чиясь здогадка, кинута на ходу. "Мабуть, проблема в кеші" - це версія, а не діагноз.

Тому в зовнішнього сигналу один маршрут - через гіпотезу й перевірку:

flowchart LR A["Зовнішній сигнал"] --> B["Гіпотеза"] B --> C["Маленька зміна"] C --> D["Перевірка: код, тести"] D --> E["Артефакт у репозиторії"]

Середина маршруту - захист від моменту, коли сервіс підкинув гору context, а ви прийняли її за готовий діагноз. Жоден tool не дає права перестрибнути крок "зрозуміти, що відбувається в коді".

Правило просте: tool скорочує шлях до перевірки, але не замінює її. Перевірка замикається на коді й тестах - не на впевненості зовнішнього джерела. І знайома рамка з минулого вебінару працює і тут: текст тикета - дані, а не інструкції. Просимо витягти симптоми й кроки, а не виконати чужу гіпотезу.

Коли такий маршрут повторюється із сесії в сесію, його рутинні кроки час віддати автоматизації. Ось тут починаються hooks.


Що таке hook

Порахуйте, скільки разів за тиждень ви повторили одне й те саме: прогнати formatter після правки, перевірити, чи не поліз Claude у .env, нагадати собі на початку сесії "перевір git status". Щоразу - вручну або "Claude, не забудь".

Hook - автоматика, яка спрацьовує сама, коли всередині Claude Code відбувається подія:

MCP - двері назовні, hook - автоматика всередині дому. Як світло з датчиком руху в під'їзді: вимикачем ніхто не клацає - датчик ловить кроки і сам вмикає лампу, і тільки коли темно. Кроки - подія, "коли темно" - умова, увімкнути світло - дія.

flowchart LR A["Подія всередині Claude Code"] --> B{"Умова збіглася?"} B -->|ні| C["Hook мовчить"] B -->|так| D["Виконується команда"] D --> E["Формат, блокування, запис у журнал"]

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 працює так само.

Тобто hook - командний артефакт: чужий блокувальний хук видно в diff до того, як він почне заважати.

Хуки вміють приїжджати і з plugin, і у frontmatter конкретного skill або агента - тоді вони активні, доки активний їхній носій. Знайома логіка доставки з шостого вебінару.

Карта подій: три групи

Подій кілька десятків, заучувати список не треба. Тримайте карту з трьох груп - майже всі корисні хуки потрапляють в одну з них:

ГрупаПодіїНавіщо
до дії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 - успіх.

Запам'ятайте точно: блокує саме 2. Не "будь-який ненульовий" - саме 2. Це часте джерело хуків, які "ніби блокують, але ні".


Matcher: вузько краще, ніж широко

Найшумніші хуки народжуються не з поганих команд, а з широких matcher: "на всі інструменти" сипле повідомленнями на кожну дію, доки їх не перестають помічати. Далі хук можна не вимикати - його вже ніхто не чує.

Для подій-інструментів matcher - ім'я інструмента, можна кілька і можна regex:

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

Далі журнал і сигнал:

Ці три сценарії - банер, журнал, сигнал - рівно те, що ви зберете в практиці.


Сходи розгортання

Blocking-хук із широкою умовою - найшвидший спосіб зненавидіти власну автоматизацію: він зупинить і небезпечне, і нормальне, і ви сперечатиметеся з власним шлагбаумом. Тому хук дорослішає сходами, а не народжується блокувальним:

flowchart LR A["Log-only: пише 'я б спрацював'"] --> B["Non-blocking: формат, звіт, підказка"] B --> C["Narrow blocking: блок вузького випадку, exit 2"]

На першій сходинці хук нічого не змінює - тільки пише, що спрацював би. Нудно, але безцінно: одразу видно, чи не занадто широкий matcher. На другій - робить безпечну дію. І тільки на третій отримує право сказати "стоп", причому на вузький випадок на кшталт rm.


Хук мовчить: п'ять питань

Хуки ламаються мовчки: немає червоного банера "hook failed, вивчіть проблему". Просто перестав форматуватися код - і все. Тому налагодження - не інтуїція, а п'ять питань по черзі:

  1. чи завантажений? - /hooks: чи є він у списку. Часта причина - битий JSON: одна зайва кома, і конфіг мовчки ігнорується;
  2. чи та подія? - PreToolUse і PostToolUse легко переплутати: хук чекає "до", а ви дивитеся "після";
  3. чи збігся matcher? - регістр букв, Bash не bash; правили через Edit, а ловите Write;
  4. чи відпрацював скрипт? - перевіряється вручну за 10 секунд;
  5. чи немає 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-кодами і виводом.

Налагодження можна делегувати самому Claude: попросіть "розберися, чому хук мовчить" - він прочитає settings.json, упіймає битий JSON і регістр у matcher, прожене скрипт вручну із тестовим JSON. Але фінальна перевірка за вами: /hooks і transcript - інтерфейс для людини. Налаштування перечитуються на льоту, а /hooks потрібен, щоб перевірити, що конфіг завантажився саме так, як ви очікували.

Recovery: вимкнути, відкотити, зафіксувати

Зрілість автоматизації перевіряється не тим, як вона працює, а тим, як швидко ви можете її зупинити. Автоматизація без шляху назад не готова до життя. Один випадок Claude Code страхує сам: Stop-хук, який щоразу відповідає "продовжуй працювати", після 8 блокувань поспіль перекривається - але решту побічок вимикати вам.

Швидко зняти навантаження, коли хук заважає прямо зараз:

Повернули собі робочу сесію - тепер спокійний фікс: відкат хука на сходинку нижче, звужений matcher, guard у скрипті. І короткий запис поруч із хуком - чому звузили: за місяць ніхто не згадає, навіщо в скрипті виняток для generated-файлів.

Автоматизація без кнопки stop - як розумна колонка, яка не розуміє слово "досить": доки не висмикнете з розетки, це не помічник, а сусід.


Практика: завдання і мета

Тепер ваш хід. На минулому вебінарі ви будували Claude доступи назовні; тепер будуєте автоматику всередині. Інструмент цієї практики - обв'язка проєкту: набір хуків, які реагують на події і додають проєкту зручність, безпеку та пам'ять.

Завдання: іграшковий репозиторій - маленький передбачуваний проєкт зі стартового комплекту, де є що правити, видаляти й логувати, - плюс .claude/settings.json із хуками та скрипти-обробники. Мета: прожити всі три групи подій і розгорнути хоча б один хук сходами, а не одразу blocking.

Фокус суворо на хуках: жодного докера і БД в основній частині - інфраструктура не має з'їсти час, відведений на автоматику. Мультиагентність і pipeline сюди теж не тягнемо - це теми наступних вебінарів. І кожен хук відповідає на питання "навіщо це в житті" - зручність, безпека або пам'ять, а не "ось механіка події".

До кінця практики в репозиторії мають лежати:


Практика: три хуки на три групи

Три базові хуки - по одному на групу подій, і в кожного життєва користь, а не демонстрація механіки:

ГрупаХукПодіяНавіщо в житті
післяСигнал завершення - звук або повідомлення, коли Claude закінчивStopШІ працює довго: займайтеся своїми справами, повертайтеся за сигналом
дозапобіжник на rm - блок через exit 2PreToolUse + matcher Bashзахист від незворотного: не дасть знести файл, над яким сиділи
сесіяжурнал у repo: prompts і ключові рішенняUserPromptSubmitпам'ять і звітність: через тиждень видно, що й чому наробили

Технічні застереження - чесні обмеження, вони теж частина інженерії:


Практика: демо-прогони і підсумок

Підсумок практики - не JSON, а докази, що автоматика жила. Чотири демо-прогони в log/:

Зафіксуйте сходи в README: як before-хук пройшов log-only -> non-blocking -> вузький blocking, а не народився блокувальним.

Чек-лист самоперевірки:

Бонуси для швидких - за бажанням: захист БД від DROP і DELETE - підняти postgres у докері силами Claude Code, before-хук на Bash/psql-команду блокує небезпечний виклик; банер SessionStart "на чому зупинилися"; будь-який свій - захист файлів за маскою, автоформат після правки.

Вийшло, якщо за README будь-хто за хвилину розуміє: які події покриті, що автоматика робить, чому їй можна довіряти - і як її вимкнути. Це і є "обв'язка проєкту як інженерний артефакт".