SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-15

Hooks Claude Code: події, exit code 2 і безпека

Дізнайтеся, де зберігаються hooks Claude Code, які події їх запускають, як exit code 2 скасовує виклик інструмента та які ризики безпеки це створює.

Що таке hook Claude Code

Hooks Claude Code — це shell-команди, які Claude Code автоматично запускає на визначених етапах власного життєвого циклу. У цьому полягає вся відмінність між hook і файлом правил. Інструкція в CLAUDE.md є порадою, і модель оцінює її разом з усім іншим контекстом. Hook — це код, який виконується незалежно від рішення моделі. Якщо ваш агент постійно пропускає formatter, про який ви вже двічі йому повідомляли, потрібна не наполегливіша інструкція. Потрібен hook.

Механізм простий. У settings-файлі ви реєструєте команду під назвою події. Коли подія виникає, Claude Code запускає вашу команду та записує дані події у стандартний ввід (stdin) у форматі JSON (JavaScript object notation). Команда читає ці дані, виконує потрібну роботу й повертає статус завершення. Exit 2 для hook PreToolUse скасовує виклик інструмента до його запуску, а все, що скрипт записав у стандартний вивід помилок (stderr), передається моделі як причина.

Назви подій і полів у цьому матеріалі взято з довідника hooks Claude Code, перевіреного в August 2026 для release 2.1.232. Цей інтерфейс швидко змінюється, тому перед копіюванням JSON із будь-якої blog post, зокрема цієї, перевірте довідник для власної версії. Виведіть свою версію за допомогою claude --version.

Де зберігається конфігурація hooks

Hook — це JSON-блок у файлі налаштувань. Його можна розмістити в одному з шести місць, а область дії файла визначає область дії hook.

  • ~/.claude/settings.json: усі проєкти на вашому комп’ютері, але не на комп’ютерах інших користувачів.
  • .claude/settings.json: один проєкт, зафіксований у репозиторії, тому hook отримує кожен, хто його клонуватиме.
  • .claude/settings.local.json: один проєкт, лише на вашому комп’ютері.
  • Керовані налаштування політик: для всієї організації, задаються адміністратором.
  • hooks/hooks.json у складі plugin; діє, доки plugin увімкнений.
  • Frontmatter skill або subagent; діє, доки цей компонент активний.

Записи hooks із цих файлів об’єднуються, а не перезаписують один одного. Файл налаштувань проєкту додає свої hooks до hooks із користувацьких налаштувань, а не замінює їх. Тому одна подія може містити кілька hooks із різних файлів. Параметр "disableAllHooks": true вимикає їх, за одним винятком: hooks із керованих налаштувань політик продовжують працювати, якщо цей параметр також не застосовано в керованих налаштуваннях.

Виконайте /hooks у межах сесії, щоб переглянути всі зареєстровані hooks, згруповані за подіями, із файлом-джерелом і matcher для кожного. Це меню доступне лише для читання, тому для зміни hook відредагуйте файл налаштувань. File watcher зазвичай виявляє зміни без перезапуску.

Які події hook існують у Claude Code

У релізі 2.1.232 перелічено тридцять одну подію — від SessionStart до SessionEnd. Вони охоплюють compaction, subagents, worktrees і файли конфігурації. Для роботи із сервером потрібні лише деякі з них.

  • PreToolUse: перед виконанням виклику tool. Це подія, яка може його заблокувати.
  • PostToolUse: після успішного виконання виклику tool. PostToolUseFailure спрацьовує замість неї, якщо виклик завершується помилкою. Тому hook, який має бачити кожен результат, потребує обох подій.
  • PermissionRequest: коли для виклику tool потрібне рішення щодо дозволу. Саме в цей момент з’явився б запит на підтвердження.
  • UserPromptSubmit: після надсилання prompt, але до його обробки Claude. Усе, що цей hook виводить у stdout, додається до контексту моделі.
  • SessionStart і SessionEnd: на кожному завершенні сесії. SessionStart також спрацьовує після compaction, якщо значення matcher дорівнює compact.
  • Stop: коли Claude завершує відповідь. Це відбувається один раз за turn, а не один раз за завершене завдання.

Кожна група має matcher, який визначає, для яких випадків запускається hook. Для подій tool він фільтрує події за назвою tool, тому "Edit|Write" спрацьовує під час редагування файлів і більше ні на що. Matchers чутливі до регістру. Порожній matcher спрацьовує під час кожного випадку. Tools із MCP (model context protocol) server мають назви у форматі mcp__<server>__<tool>. Тому matcher "mcp__github__.*" охоплює tools одного сервера й не охоплює tools інших серверів.

Stop hooks мають особливість, яку потрібно врахувати до їх написання. Stop hook, що блокує виконання, повертає модель до роботи, а Claude Code примусово ігнорує hook після восьми послідовних блокувань. Прочитайте поле stop_hook_active із вхідних даних hook і завершіть роботу з кодом 0, якщо його значення true. Інакше hook працюватиме в циклі, доки не досягне цього ліміту.

Що отримує hook через stdin

Коли Claude збирається виконати npm test, hook PreToolUse на Bash читає зі stdin такі дані:

{
  "session_id": "abc123",
  "cwd": "/home/deploy/myproject",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

Кожна подія містить session_id, cwd, permission_mode, transcript_path і hook_event_name. Події інструментів додатково містять tool_name, tool_input і tool_use_id. Інші події мають власні поля: UserPromptSubmit отримує текст prompt, а SessionStartsource зі значенням startup, resume, clear, compact або fork.

jq — стандартний спосіб прочитати ці дані в shell-скрипті, але в мінімальному образі сервера цієї утиліти немає. Спочатку встановіть її за допомогою sudo apt install -y jq в Ubuntu і Debian.

Що робить код завершення з поточним викликом інструмента

Є три результати.

  • Exit 0 означає, що ваш hook не заперечує. Для PreToolUse це не те саме, що схвалення, і звичайний процес перевірки дозволів усе одно виконується. Для UserPromptSubmit і SessionStart stdout додається до контексту моделі.
  • Exit 2 блокує дію для подій, які можна блокувати, зокрема PreToolUse, а stderr стає причиною, яку показують моделі. Для подій, які не можна блокувати, наприклад PostToolUse, блокування ігнорується, але stderr усе одно передається моделі як зворотний зв’язок.
  • Будь-який інший код завершення означає неблокувальну помилку. Дія виконується. У транскрипті з’являється повідомлення про помилку hook, яке містить перший рядок stderr після тексту Failed with non-blocking status code:.

Якщо потрібно зробити щось більше, ніж заблокувати дію або не втручатися, використовуйте exit 0 і виведіть об’єкт JSON у stdout. Hook PreToolUse приймає рішення за допомогою permissionDecision:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database drops go through a migration, not through the agent."
  }
}

"allow" пропускає інтерактивний запит, "deny" скасовує виклик і передає причину моделі, а "ask" показує запит у звичайному режимі. Для кожного hook вибирайте один стиль. Якщо поєднати exit 2 із рішенням JSON у stdout, результат доведеться визначати окремо.

Якщо одна подія відповідає кільком hook, вони запускаються паралельно, і кожен із них виконується до завершення. deny одного hook не зупиняє інші, тому logging hook усе одно записує свій рядок, а guardrail hook забороняє той самий виклик. Після цього Claude Code об’єднує відповіді та залишає найсуворіше рішення в такому порядку: deny, defer, ask, allow.

Приклад 1: заблокувати небезпечну команду до її виконання

Збережіть цей скрипт у файлі .claude/hooks/block-destructive.sh у своєму проєкті:

#!/bin/bash
# Deny a Bash tool call whose command matches a banned pattern.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

for pattern in 'rm -rf /' 'mkfs' 'dd if=' 'DROP TABLE'; do
  if printf '%s' "$COMMAND" | grep -qiF -- "$pattern"; then
    echo "Blocked by policy: the command matches '$pattern'. A human runs this one." >&2
    exit 2
  fi
done

exit 0

Зробіть файл виконуваним, а потім зареєструйте його в PreToolUse у .claude/settings.json:

chmod +x .claude/hooks/block-destructive.sh
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
            "timeout": 10,
            "statusMessage": "Checking the command against policy"
          }
        ]
      }
    ]
  }
}

Перевірте скрипт вручну, перш ніж довіряти йому, оскільки hook, який аварійно завершується на власних вхідних даних, пропускає виконання:

echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
  | .claude/hooks/block-destructive.sh
echo $?

У stderr має з’явитися рядок Blocked by policy:, а код завершення має дорівнювати 2. Передайте йому нешкідливу команду, наприклад ls -la. У такому разі вивід має бути відсутнім, а код завершення має дорівнювати 0. У сеансі заборонений виклик з’являється в transcript із вашим повідомленням як причиною, а модель читає це повідомлення й адаптує свої дії.

Ця властивість робить такий підхід корисним: hooks PreToolUse спрацьовують до перевірки permission mode у будь-якому режимі дозволів, тому заборона діє навіть у режимі bypassPermissions. Саме тому hook корисний разом із автоматичним режимом Claude Code і його налаштуваннями дозволів, коли кількість запитів на підтвердження зменшено, але hook усе одно спрацьовує.

Важливо правильно розуміти призначення цього механізму. Порівняння шаблонів із рядком команди є захисним обмеженням від необережних дій агента. Воно не є межею захисту від кмітливого агента, оскільки ту саму команду можна записати у формі, яку ваш grep не побачить. Жорсткі правила мають визначатися в системі дозволів і в обліковому записі, від імені якого працює процес.

Приклад 2: форматування та перевірка після кожного редагування

PostToolUse із засобом зіставлення Edit|Write запускається після будь-якого інструмента редагування файлів. Збережіть це як .claude/hooks/after-edit.sh:

#!/bin/bash
# Format the edited file, then report lint failures back to the model.
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0

case "$FILE" in
  *.py)
    ruff format "$FILE" >/dev/null 2>&1
    if ! ruff check "$FILE" >&2; then
      exit 2
    fi
    ;;
  *.sh)
    if ! shellcheck "$FILE" >&2; then
      exit 2
    fi
    ;;
esac

exit 0
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/after-edit.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Попросіть Claude додати до файлу Python функцію з неправильними відступами, а потім відкрийте файл. Він буде відформатований. Це підтверджує, що hook спрацював, оскільки успішний hook не відображає нічого в діалозі.

Код завершення 2 не скасовує жодних змін. PostToolUse запускається після виконання інструмента, тому редагування все одно записується на диск. Код завершення 2 потрібен для того, щоб вивід ruff check передавався моделі як зворотний зв’язок. Завдяки цьому вона виправляє щойно внесену помилку, а не переходить до наступного кроку. У цьому полягає різниця між помилкою lint, яку ви виявляєте під час commit, і помилкою, яку агент виправляє в тому самому циклі.

Тут важливі два обмеження matcher. Edit|Write не бачить файлів, змінених shell-командою, а Claude достатньо часто записує файли через Bash, щоб ця прогалина мала значення. Для охоплення кожного виклику також зіставляйте Bash і нехай скрипт виводить список змінених файлів за допомогою git status --porcelain. Для охоплення один раз за цикл помістіть сканування в hook Stop.

Приклад 3: журналювання кожного виклику інструмента для аудиту

Порожній matcher у PostToolUse спрацьовує для кожного інструмента. Надсилання запису до системного журналу, а не у файл у домашньому каталозі, не дає самому shell агента отримати до нього доступ:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{time: now|todate, session: .session_id, cwd: .cwd, tool: .tool_name, input: .tool_input}' | logger -t claude-code -p local0.info"
          }
        ]
      }
    ]
  }
}

Прочитайте записи за допомогою journalctl -t claude-code -o cat | tail -n 5. Ви маєте побачити один рядок JSON для кожного виклику інструмента; найновіший запис буде останнім. Якщо нічого не з’явилося, hook не виконався. Нижче наведено розділ із діагностикою цієї проблеми.

Додайте такий самий блок під PostToolUseFailure, щоб реєструвати виклики, які завершилися помилкою, оскільки PostToolUse спрацьовує лише після успішного виконання, а невдала команда зазвичай є найважливішою для аналізу. Причина використання logger замість дописування у файл у домашньому каталозі полягає у правах власності: hook виконується від імені того самого користувача, що й shell агента, тому все, до чого цей користувач може дописувати, він також може очистити. Журнал записує systemd-journald від імені власного облікового запису.

Тривалість виконання hook

ChartDefault hook timeout in seconds, by hook type and event
The data behind this chart
[
  {
    "label": "command, http or mcp_tool hook",
    "default_timeout_seconds": 600
  },
  {
    "label": "agent hook",
    "default_timeout_seconds": 60
  },
  {
    "label": "prompt hook",
    "default_timeout_seconds": 30
  },
  {
    "label": "command hook on UserPromptSubmit",
    "default_timeout_seconds": 30
  },
  {
    "label": "command hook on MessageDisplay",
    "default_timeout_seconds": 10
  },
  {
    "label": "any hook on SessionEnd",
    "default_timeout_seconds": 1.5
  }
]

За замовчуванням command hook має 600 секунд, тобто десять хвилин. Для деяких подій цей час значно менший. Усі SessionEnd hooks спільно мають бюджет 1.5 секунд, тому очищення наприкінці сесії має виконуватися швидко. Водночас довше значення timeout для hook збільшує цей спільний бюджет відповідно, але не більш як до 60 секунд.

Якщо hook перевищує ліміт часу, його скасовано, і він не повертає жодного рішення. Для PreToolUse guardrail це означає, що він не блокує виконання: виклик інструмента продовжується у звичайному процесі перевірки дозволів. Тому guardrail-скрипти мають бути короткими. Для повільних операцій, результату яких ніхто не очікує, наприклад надсилання журналу в інше місце, задайте "async": true. Тоді hook виконуватиметься у фоновому режимі й не затримуватиме виклик інструмента.

Хуки, файли правил, skills і MCP-сервери

Ці чотири механізми часто плутають, оскільки всі вони змінюють поведінку агента. Лише один із них перестає бути рекомендацією.

Файл правил (CLAUDE.md або файл у каталозі .claude/rules/) — це текст, який завантажується в контекст моделі. Він формує поведінку, але нічого не забезпечує примусово. Під час довгої розмови, роботи з великим diff і надходження нового запиту користувача один рядок із такого файла може втратити пріоритет. Саме так зазвичай виникає ігнорування агентами записаних вами інструкцій.

Skill — це каталог з інструкціями та скриптами, який модель завантажує, коли вважає skill релевантним. Саме це рішення є призначенням skill і водночас його обмеженням: модель усе одно вирішує сама. Це видно на прикладі skill Ponytail, який спрямовує агента до найменшої працездатної зміни: він визначає підхід до всього завдання, чого не може зробити жоден hook, але діє лише тоді, коли модель вирішує його завантажити.

MCP (model context protocol) server надає моделі нові інструменти для виклику. Він розширює доступ агента до ресурсів. Але він не змушує агента використовувати ці ресурси. Це окремий процес, яким потрібно керувати; це окрема операційна задача: див. запуск MCP-серверів на VPS.

Hook — єдиний із чотирьох механізмів, який запускається без рішення моделі. Використовуйте файл правил для бажаної поведінки, а skill — для процедури, якої модель має дотримуватися, коли вона застосовна. Використовуйте hook для кроку, який має виконуватися щоразу, або для дії, яка ніколи не повинна виконуватися. Розширене порівняння, зокрема випадки, коли skill кращий за файл правил, наведено в матеріалі порівняння skills, MCP і файлів правил.

Plugin — це спосіб пакування, а не п’ятий механізм. Він об’єднує hooks і skills в один інстальований компонент. Так команда розгортає однаковий захисний механізм на кожній машині: див. як працюють plugins у Claude Code.

Рішення щодо безпеки на спільному VPS

Hook — це код, який запускає агент. Він виконується від імені користувача, який запустив Claude Code. Hook успадковує середовище та права доступу до файлів цього користувача. На ноутбуці це питання робочого процесу. На VPS, де агент працює без нагляду, це питання безпеки, яке має чотири практичні аспекти.

Hook у репозиторії — це код, написаний не вами. .claude/settings.json зберігається в репозиторії, тому клонування репозиторію та запуск сеансу в ньому може зареєструвати hook, який входить до складу репозиторію. Claude Code вимагає підтвердити довіру до робочого простору перед запуском project hooks для цієї папки. Отже, саме підтвердження довіри є моментом, коли ви вирішуєте їх запустити. Спочатку прочитайте блок hooks.

Hook бачить повний вхідний параметр інструмента. Audit hook, який записує tool_input, зберігає у файлі кожен аргумент кожної команди, зокрема будь-який токен, який опинився в командному рядку. Такий журнал потребує такого самого захисту, як і сам секрет. Це частина ширшої проблеми зберігання секретів поза досяжністю AI-агента.

Hook може записувати дані в контекст моделі. Усе, що hook SessionStart або UserPromptSubmit виводить у stdout, додається до розмови. Hook, який передає текст із зовнішнього джерела, системи відстеження задач або файлу журналу, надсилає моделі ненадійний текст так, ніби ви ввели його самостійно. Розглядайте такий stdout як вхідні дані, а не як результат.

Вирішальним засобом контролю є рівень привілеїв. Запускайте агента від імені окремого непривілейованого користувача, якому надано лише потрібні правила sudo. Заборона PreToolUse корисна, але за задумом вона працює за принципом best effort: у довідці це саме сказано про фільтр if, а для жорсткої заборони рекомендується використовувати систему дозволів. Правила дозволів і обліковий запис, від імені якого працює процес, — це механізми, які залишаються чинними за несприятливих умов.

В одній властивості можна бути впевненим у будь-якій конфігурації. PreToolUse hooks спрацьовують до перевірки permission mode у кожному режимі дозволів, тому hook, який повертає deny, блокує інструмент навіть у режимі bypassPermissions. Hooks можуть посилювати обмеження, встановлені правилами дозволів. Послабити їх вони не можуть.

Чому мій hook не спрацьовує?

Виконуйте ці дії по порядку. Кожен крок описує симптом, який ви побачите.

  • Виконайте /hooks і перевірте, чи відображається hook для очікуваної події. Якщо hook відсутній у меню, це зазвичай означає, що у файлі налаштувань є синтаксична помилка JSON, оскільки кінцеві коми та коментарі не дозволені, або файл відсутній в одному із шести наведених вище розташувань.
  • Точно порівняйте matcher з назвою інструмента. Matcher чутливий до регістру, тому "bash" ніколи не відповідає інструменту Bash.
  • Запустіть скрипт вручну із тестовими вхідними даними, як у прикладі 1 вище. Неочікуваний код завершення означає помилку у вашому скрипті. Claude Code повідомляє її як помилку hook, а не як результат перевірки.
  • Повідомлення jq: command not found означає, що на цій машині відсутній jq. Повідомлення command not found для вашого скрипту означає, що шлях не вдалося визначити, тому використовуйте ${CLAUDE_PROJECT_DIR} або абсолютний шлях. Якщо скрипт взагалі не запускається, імовірно, він не має права на виконання.
  • Hook виводить коректний JSON, але нічого не відбувається. Shell-form hook запускається через sh -c. Якщо профіль оболонки виводить банер, цей банер додається перед вашим JSON. Stdout більше не починається з {, тому Claude Code читає весь вивід як звичайний текст та ігнорує результат. При коді завершення 0 ніде нічого не повідомляється, крім debug log. Обгорніть будь-який echo у профілі так, щоб він виконувався лише в інтерактивних оболонках.
  • Якщо проблема не зникає, запустіть сесію з claude --debug-file /tmp/claude.log і виконайте tail -f /tmp/claude.log у другому терміналі. Debug log записує, які hook відповідали умовам, який код завершення повернув кожен із них, а також усе, що вони вивели в stdout і stderr.

FAQ

Яка різниця між hook Claude Code та інструкцією CLAUDE.md?

Інструкція CLAUDE.md — це текст у контексті моделі. Тому вона конкурує за увагу з розмовою та поточним запитом, а модель може зіставляти її з ними. Hook — це shell-команда, яку Claude Code запускає у визначений момент свого життєвого циклу. Вона виконується під час кожного виникнення відповідної події, незалежно від рішення моделі. Використовуйте інструкцію для бажаної поведінки. Використовуйте hook для кроку, який має виконуватися завжди, або дії, яка ніколи не повинна виконуватися.

Як заборонити Claude Code виконувати певну shell-команду?

Зареєструйте hook PreToolUse із matcher Bash, який читає команду з .tool_input.command, записує причину в stderr і завершується з кодом 2. Claude Code скасовує виклик і показує моделі вашу причину. Це відбувається до перевірки permission mode, тому заборона діє навіть у режимі bypassPermissions. Порівняння з рядком команди є запобіжним обмеженням, а не межею безпеки, оскільки ту саму команду можна записати у формі, яку шаблон не розпізнає. Тому додатково використовуйте permission rules і непривілейований обліковий запис.

Hook виводить коректний JSON, але нічого не відбувається. Чому?

Найпоширеніша причина — профіль shell. Hook без поля args запускається через sh -c, а деякі профілі виводять банер під час кожного запуску shell. Цей банер потрапляє в stdout перед вашим JSON. Оскільки вивід більше не починається з {, Claude Code сприймає весь вивід як звичайний текст і ігнорує рішення. У разі завершення з кодом 0 у transcript взагалі нічого не записується. Захистіть будь-який echo у профілі перевіркою інтерактивності shell, а потім підтвердьте виправлення, переглянувши debug log із claude --debug-file /tmp/claude.log.

Чи безпечно запускати hook Claude Code на спільному сервері?

Hook запускається від імені користувача, який запустив Claude Code, і має права доступу до файлів цього користувача. Тому hook може виконати будь-яку дію, доступну цьому обліковому запису. Для більшості ризиків достатньо двох заходів: запускайте агента від імені окремого непривілейованого облікового запису з вузькою політикою sudo і читайте блок hooks у кожному repository, перш ніж приймати діалог підтвердження довіри до workspace, оскільки project hooks зберігаються в .claude/settings.json. Установіть "disableAllHooks": true у файлі settings, якщо потрібно, щоб жоден із них не запускався.