Hooks Claude Code: події, exit code 2 і безпека
Дізнайтеся, де налаштовуються hooks Claude Code, які події їх запускають, як exit code 2 скасовує виклик tool і які ризики безпеки це створює.
Що таке hook Claude Code
Hooks Claude Code — це shell-команди, які Claude Code самостійно запускає у визначені моменти власного життєвого циклу. У цьому полягає вся відмінність між hook і файлом правил. Інструкція у CLAUDE.md — це порада, і модель оцінює її разом з усім іншим контекстом. Hook — це код, який виконується незалежно від того, погоджується з ним модель чи ні. Якщо ваш агент постійно пропускає formatter, про який ви вже двічі йому повідомили, вам не потрібна суворіша інструкція. Вам потрібен hook.
Механізм простий. Ви реєструєте команду у файлі налаштувань під назвою події. Коли ця подія відбувається, Claude Code запускає вашу команду та записує дані події у її стандартний ввід (stdin) у форматі JSON (JavaScript object notation). Команда читає ці дані, виконує потрібну роботу й повертає код завершення. Код завершення 2 для hook PreToolUse скасовує виклик tool до його запуску, а все, що ваш скрипт записав у стандартний потік помилок (stderr), передається моделі як причина.
Назви подій і полів у цьому матеріалі взято з довідника hooks Claude Code, перевіреного в August 2026 для release 2.1.232. Цей інтерфейс швидко змінюється, тому перед копіюванням JSON із будь-якого допису в блозі, зокрема цього, перевірте довідник для власної версії. Виведіть свою версію командою claude --version.
Де зберігається конфігурація hook
Hook — це JSON-блок у файлі налаштувань. Його можна розмістити в шести місцях, а область дії hook визначається областю дії файлу.
~/.claude/settings.json: усі проєкти на вашому комп’ютері й більше ніде..claude/settings.json: один проєкт, доданий до репозиторію, тому hook отримує кожен, хто його клонує..claude/settings.local.json: один проєкт, лише на вашому комп’ютері.- Керовані налаштування політики: для всієї організації, задаються адміністратором.
hooks/hooks.jsonусередині plugin: діє, доки plugin увімкнено.- Frontmatter skill або subagent: діє, доки відповідний компонент активний.
Записи hook у цих файлах об’єднуються, а не замінюють один одного. Файл налаштувань проєкту додає свої hook до hook у користувацьких налаштуваннях, а не замінює їх. Тому одна подія може містити кілька hook із різних файлів. Параметр "disableAllHooks": true вимикає їх, за одним винятком: hook із керованих налаштувань політики продовжують працювати, якщо цей параметр також не задано в керованих налаштуваннях.
Виконайте /hooks у межах сесії, щоб переглянути всі зареєстровані hook, згруповані за подіями, із файлом-джерелом і matcher для кожного. Меню доступне лише для читання, тому змінюйте hook шляхом редагування файлу налаштувань. File watcher зазвичай підхоплює зміни без перезапуску.
Які події hook доступні в Claude Code
У релізі 2.1.232 наведено тридцять одну подію — від SessionStart до SessionEnd. Вони охоплюють compaction, subagents, worktrees і configuration files. Для роботи із серверами потрібна лише частина з них.
PreToolUse: перед виконанням виклику tool. Це єдина подія, яка може його заблокувати.PostToolUse: після успішного виконання виклику tool.PostToolUseFailureспрацьовує, якщо виклик завершується помилкою. Тому hook, який має бачити кожен результат, повинен обробляти обидві події.PermissionRequest: коли для виклику tool потрібне рішення щодо дозволу. У цей момент з’явився б запит на підтвердження.UserPromptSubmit: під час надсилання prompt, до початку його обробки Claude. Усе, що цей hook виводить у stdout, додається до контексту моделі.SessionStartіSessionEnd: на кожному завершенні session.SessionStartтакож спрацьовує після compaction із значенням matchercompact.Stop: коли Claude завершує відповідь. Подія спрацьовує один раз за turn, а не один раз після завершення task.
Кожна група має matcher, який визначає, для яких випадків запускається hook. У подіях tool він фільтрує події за назвою tool, тому "Edit|Write" спрацьовує під час редагування файлів і більше ні під час яких операцій. Matchers чутливі до регістру. Порожній matcher спрацьовує під час кожного випадку. Tools із сервера MCP (model context protocol) мають назви у форматі 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, а SessionStart — source зі значенням startup, resume, clear, compact або fork.
jq — стандартний спосіб прочитати ці дані в shell-скрипті, але в мінімальному образі сервера його немає. Спочатку встановіть його за допомогою sudo apt install -y jq в Ubuntu і Debian.
Як exit status впливає на поточний виклик інструмента
Можливі три результати.
- Exit 0 означає, що hook не заперечує проти дії. Для
PreToolUseце не означає схвалення: звичайний permission flow все одно запускається. ДляUserPromptSubmitіSessionStartstdout додається до контексту моделі. - Exit 2 блокує дію для подій, які можна блокувати, зокрема
PreToolUse, а stderr стає причиною, яку показують моделі. Для подій, які не можна блокувати, наприкладPostToolUse, блокування ігнорується, але stderr все одно передається моделі як feedback. - Будь-який інший exit code означає неблокувальну помилку. Дія продовжується. У transcript відображається повідомлення про помилку hook, яке містить перший рядок stderr після тексту
Failed with non-blocking status code:.
Якщо потрібно не лише заблокувати дію або не втручатися, використовуйте exit 0 і виведіть у stdout JSON object. Hook PreToolUse приймає рішення за допомогою permissionDecision:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database drops go through a migration, not through the agent."
}
}"allow" пропускає інтерактивний prompt, "deny" скасовує виклик і надсилає причину моделі, а "ask" показує prompt у звичайному режимі. Для кожного hook використовуйте один стиль. Якщо поєднати exit 2 із JSON decision у 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: форматування та lint після кожного редагування
PostToolUse із matcher 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 нічого не виводить у розмові.
Exit 2 у цьому випадку нічого не скасовує. PostToolUse спрацьовує після завершення роботи інструмента, тому зміни все одно записуються на диск. Натомість ruff check передає моделі вивід як зворотний зв’язок, і вона виправляє щойно внесену помилку, а не переходить далі. Саме це відрізняє помилку lint, яку виявляють під час commit, від помилки, яку agent виправляє в тому самому turn.
Тут важливі два обмеження matcher. Edit|Write не бачить файлів, змінених shell-командою, а Claude достатньо часто записує файли через Bash, щоб ця прогалина мала значення. Для покриття кожного виклику також зіставте Bash і доручіть скрипту перелічувати змінені файли за допомогою git status --porcelain. Для покриття один раз за turn натомість розмістіть сканування в 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
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 секунд, тому очищення наприкінці сесії має виконуватися швидко. Якщо для hook установити довший timeout, спільний бюджет збільшується до такого самого значення, але не більше ніж до 60 секунд.
Hook, який перевищує ліміт часу, скасовується й не формує рішення. Для PreToolUse guardrail це означає, що він не блокує виконання: виклик інструмента продовжується через звичайний процес перевірки дозволів. Тому guardrail-скрипти мають бути короткими. Для повільних операцій, результату яких ніхто не очікує, наприклад передавання журналу в інше місце, установіть "async": true. Тоді hook виконується у фоновому режимі й не затримує виклик інструмента.
Хуки, файли правил, навички та MCP-сервери
Чотири поняття часто плутають між собою, оскільки всі вони змінюють поведінку агента. Лише один із цих механізмів перестає бути рекомендацією.
Файл правил (CLAUDE.md або файл у каталозі .claude/rules/) — це текст, який завантажується в контекст моделі. Він формує поведінку, але нічого не забезпечує примусово. Під час довгої розмови, роботи з великим diff і надходження нового запиту користувача один його рядок може бути проігнорований. Саме так зазвичай виникає ситуація, коли агенти ігнорують записані вами інструкції.
Навичка — це каталог з інструкціями та скриптами, який модель завантажує, коли вважає навичку релевантною. Саме це рішення є суттю навички та водночас її обмеженням: модель усе одно вирішує сама. Обидві властивості видно на прикладі навички Ponytail, яка спрямовує агента до найменшої працездатної зміни: вона визначає підхід до виконання всього завдання так, як не зміг би жоден хук, але працює лише тоді, коли модель вирішує її завантажити.
MCP (model context protocol) сервер надає моделі нові інструменти для виклику. Він розширює доступ агента до ресурсів. Але він не змушує агента використовувати ці ресурси. Крім того, це окремий процес, яким потрібно керувати, а це самостійне завдання: див. запуск MCP-серверів на VPS.
Хук — єдиний із чотирьох механізмів, який запускається без рішення моделі. Використовуйте файл правил для побажання, а навичку — для процедури, якої модель має дотримуватися за відповідних умов. Використовуйте хук для кроку, який має виконуватися щоразу, або для дії, яка ніколи не повинна виконуватися. Докладніше порівняння, зокрема випадки, коли навичка краща за файл правил, наведено в матеріалі порівняння навичок, MCP і файлів правил.
Плагін — це спосіб пакування, а не п’ятий механізм. Він об’єднує хуки та навички в один встановлюваний компонент. Так команда розгортає однаковий захисний механізм на кожній машині: див. як працюють плагіни Claude Code.
Рішення щодо безпеки на спільному VPS
Hook — це код, який запускає агент. Він виконується від імені користувача, який запустив Claude Code. Hook успадковує середовище та права доступу до файлів цього користувача. На ноутбуці це питання організації робочого процесу. На VPS, де агент працює без нагляду, це питання безпеки, яке має чотири практичні складові.
Hook у репозиторії — це код, якого ви не писали. .claude/settings.json зберігається в репозиторії, тому клонування репозиторію та запуск сесії в ньому може зареєструвати hook, що входив до складу репозиторію. Claude Code запускає project hooks лише після підтвердження довіри до робочої області для цієї папки. Отже, саме прийняття довіри є моментом, коли ви дозволяєте їх виконання. Спочатку прочитайте блок hooks.
Hook бачить усі вхідні дані інструмента. Audit hook, який записує tool_input, зберігає у файлі всі аргументи кожної команди, зокрема будь-який токен, що випадково опинився в командному рядку. Такий журнал потребує такого самого захисту, як і сам секрет. Це частина ширшої проблеми — не допускати секрети до агента зі штучним інтелектом.
Hook може записувати дані в контекст моделі. Усе, що hook SessionStart або UserPromptSubmit виводить у stdout, додається до розмови. Hook, який передає текст із зовнішнього джерела, системи відстеження завдань або файлу журналу, передає моделі недовірений текст так, ніби ви ввели його самостійно. Hook, який пересилає повідомлення з іншої сесії Claude Code на тому самому VPS, робить те саме. Вивід одного агента не має більшої підстави для довіри, ніж дані із системи відстеження завдань. Розглядайте цей stdout як вхідні дані, а не як результат.
Вирішальним контролем є рівень привілеїв. Запускайте агента від імені окремого непривілейованого користувача, надавши йому лише потрібні правила sudo. Заборона PreToolUse корисна, але за задумом вона працює за принципом найкращих зусиль: у документації так само зазначено про фільтр if і рекомендовано використовувати систему дозволів, якщо потрібна жорстка заборона. Правила дозволів і обліковий запис, від імені якого працює процес, — це механізми, що зберігають чинність у критичних ситуаціях.
Одна властивість справедлива для будь-якої конфігурації. PreToolUse hooks запускаються перед перевіркою режиму дозволів у кожному режимі дозволів. Тому 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, але нічого не відбувається. Hook у shell-формі запускається через
sh -c. Якщо профіль shell виводить банер, цей банер додається перед JSON. Тоді stdout більше не починається з{, тому Claude Code сприймає весь вивід як звичайний текст і ігнорує рішення. За коду завершення 0 ніде нічого не повідомляється, крім debug log. Обгорніть будь-якийechoу профілі так, щоб він виконувався лише в інтерактивних shell. - Проблему не вирішено: запустіть сесію з
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 взагалі нічого не відображається. Додайте перевірку інтерактивного shell для будь-якого echo у профілі. Потім підтвердьте виправлення, прочитавши debug log із claude --debug-file /tmp/claude.log.
Чи безпечно запускати hooks Claude Code на спільному сервері?
Hooks запускаються від імені користувача, який запустив Claude Code, і мають права доступу до файлів цього користувача. Тому hook може виконати будь-яку дію, доступну цьому обліковому запису. Для більшості ризиків достатньо двох правил: запускайте agent від імені окремого непривілейованого облікового запису з обмеженою політикою sudo і перевіряйте блок hooks у кожному repository, перш ніж приймати діалог довіри до його workspace, оскільки project hooks зберігаються всередині .claude/settings.json. Установіть "disableAllHooks": true у файлі налаштувань, якщо не потрібно запускати жоден із них.