Хуки в Claude Code - это shell-команды, HTTP-вызовы или короткие LLM-проверки, которые запускаются автоматически до или после действия Claude. Они решают то, с чем CLAUDE.md не справляется: жёстко блокируют опасные действия (удаление .env, force-push в main), форматируют код после правки, инжектят свежий контекст в начало сессии. Дальше - разбор 4 типов обработчиков, 31 события жизненного цикла, тест «когда правило идёт в hook», 7 готовых хуков под копипаст в .claude/settings.json и 3 ловушки, в которые я сам попадал.
Каждый день в Telegram-канале - что нового в вайб-кодинге: инструменты, разборы, ошибки. Подпишись, чтобы быть в курсе.
Что такое хуки в Claude Code?
Я полгода держал в CLAUDE.md правило «никогда не редактируй .env напрямую». Claude нарушал его раз в месяц. Не злонамеренно - просто забывал к концу длинной сессии. Один раз в .env уехал лишний токен, другой - переменная с дублирующим именем переписала рабочую.
Перенёс это правило в хук - забыть его стало невозможно. На каждой попытке отредактировать .env процесс Claude Code тормозит и возвращает мне ошибку. Это не подсказка, это блок.
Hooks are user-defined shell commands, HTTP endpoints, or LLM prompts that execute automatically at specific points in Claude Code's lifecycle.
Перевод: хук - это твоя команда, которую Claude Code запускает на конкретном событии. На каждом редактировании файла, запуске Bash, старте сессии, завершении субагента - есть событие, на которое можно повесить хук.
Зачем это нужно вайб-кодеру:
- Защитить файлы с секретами от случайной правки.
- Автоматически прогонять prettier/eslint после каждой генерации кода.
- Блокировать
git push --forceв main без явного разрешения. - Инжектить свежий контекст в начало каждой сессии.
- Логировать действия Claude для аудита.
CLAUDE.md решает контекст. Хуки решают действия. Это две разные сущности, и путать их - главная ошибка новичков.
Чем хуки отличаются от CLAUDE.md и скиллов?
Anthropic в документации явно проговаривает разницу между CLAUDE.md и хуками:
CLAUDE.md content is delivered as a user message after the system prompt, not as part of the system prompt itself. Claude reads it and tries to follow it, but there's no guarantee of strict compliance.
Перевожу: контент CLAUDE.md приходит в Claude как первое сообщение от пользователя, а не как системная инструкция. Модель его прочитает и постарается выполнить. Но никаких гарантий нет - особенно к концу длинной сессии, когда контекст разрастается и старые инструкции вытесняются.
Хук работает иначе. Он живёт в settings.json, не зависит от модели и запускается процессом Claude Code на конкретном событии - PreToolUse, PostToolUse, SessionStart и других. Команда выполняется до того, как Claude получит результат, и может физически заблокировать действие (exit 2) или подправить файл после правки (exit 0).
Сравнение в таблице:
| Признак | CLAUDE.md | Hook | Skill |
|---|---|---|---|
| Где живёт | CLAUDE.md в корне проекта | .claude/settings.json | .claude/skills/ папка |
| Кто запускает | Модель читает как часть user message | Процесс Claude Code на событии | Модель через инструмент Skill |
| Гарантия выполнения | Нет (best-effort) | Да (детерминированный shell exit code) | Через вызов модели (с грантом, но не shell-уровня) |
| Что подходит | Контекст проекта, тон, стиль, маршрутизация | Запреты, форматирование, проверки на конкретных файлах | Сложные многошаговые процедуры с параметрами |
| Когда срабатывает | Каждое сообщение модели в сессии | На событии жизненного цикла | По вызову модели |
| Можно ли заблокировать действие | Нет | Да (exit 2) | Нет |
Полный шаблон CLAUDE.md и 6 правил живого файла - в гайде Как настроить CLAUDE.md в 2026: готовый шаблон и 6 правил. Скиллы как именованные навыки - в Claude Code Skills: как собрать библиотеку под себя.
Хук - это нижний уровень: без модели, без интерпретации, чистый shell.
Хочешь не только настроить хуки, но и собрать связку, которая делает Claude по-настоящему предсказуемым? Хуки - лишь один из трёх инструментов контекст-инжиниринга. На практикуме за 3 эфира собираешь все три кита: ИИ-клон (твоя методология в скиллах) + Второй мозг (структура знаний проекта) + контекст-инжиниринг (Plan Mode, /compact, хуки) - именно эта связка превращает Claude из «помощника с галлюцинациями» в надёжный инструмент.
4 типа хуков: Command, HTTP, Prompt, Agent - какой выбрать?
| Тип | Что делает | Когда выбрать | Сложность |
|---|---|---|---|
| Command | Запускает shell-команду, получает JSON на stdin, отвечает exit code и stdout | 90% случаев: проверка путей, форматирование, grep по содержимому, lsof, git-операции | низкая |
| HTTP | POST-запрос на твой URL с JSON в теле, такой же JSON в ответе | Когда правило живёт в команде/CI: централизованный валидатор, общая панель статусов, удалённый аудит | средняя (нужен сервер) |
| Prompt | Отправляет короткий промпт в Haiku (по умолчанию), получает yes/no для семантической оценки | Когда grep не справляется: «оцени, выглядит ли этот текст как корпоративный» | низкая |
| Agent | Создаёт субагента с доступом к Read/Grep/Glob для многошаговой проверки | Тяжёлая валидация: «найди все изменённые файлы и проверь, что есть соответствующие тесты» | высокая |
Самая частая ошибка - пытаться сделать на Command то, что нужно делать на Prompt. Пример: «не пиши длинные тире в .md файлах» - это grep, Command идеален. А «не пиши корпоративным тоном» - это семантика, нужен Prompt.
Если только начинаешь: бери Command. 7 готовых хуков ниже - все Command. HTTP/Prompt/Agent - следующий уровень, когда дойдёшь.
Где лежит settings.json и какие 4 уровня конфигов?
| Уровень | Путь | Когда применяется | В git? |
|---|---|---|---|
| Корпоративный | /Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux) | Через MDM на всю команду | нет |
| Пользовательский | ~/.claude/settings.json | Все проекты одного юзера | нет |
| Проектный | .claude/settings.json в корне репо | Этот проект, для всей команды | да |
| Локальный | .claude/settings.local.json | Только этот проект, только этот юзер | нет (в .gitignore) |
Я держу 7 описанных ниже хуков в проектном .claude/settings.json - чтобы при клонировании репозитория хуки подтянулись автоматически, и Claude в новом окне сразу работал по тем же правилам. Личные экспериментальные хуки - в .claude/settings.local.json, чтобы не мешать команде.
Все 4 файла мерджатся при старте сессии. Если правила противоречат - более низкий уровень побеждает (локальный важнее проектного, проектный важнее пользовательского).
Самый быстрый способ создать первый хук - интерактивное меню /hooks прямо в Claude Code. Выбираешь событие из списка, выбираешь matcher (например, Bash или Edit), вставляешь команду. Claude Code сам пишет валидный JSON в settings.json.
31 событие жизненного цикла: какие нужны новичку?
Events fall into three cadences: once per session (SessionStart, SessionEnd), once per turn (UserPromptSubmit, Stop, StopFailure), and on every tool call inside the agentic loop (PreToolUse, PostToolUse).
Три каденса важно понимать, чтобы не путаться:
- Раз за сессию -
SessionStart,SessionEnd,Setup. Запускаются один раз при открытии и закрытии Claude Code. Для инициализации контекста, очистки временных файлов. - Раз за ход -
UserPromptSubmit,UserPromptExpansion,Stop,StopFailure. Срабатывают на каждое твоё сообщение и на каждую остановку модели. Для логирования диалога, финальной проверки результата. - На каждый вызов инструмента -
PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied,PostToolBatch,SubagentStart,SubagentStop. Срабатывают на каждое редактирование файла, запуск Bash, чтение, поиск. Это самый горячий слой - где живут запреты, форматирование, валидации.
Полный список из 31 события включает асинхронные/реактивные: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. Это для продвинутых сценариев - пока пропусти.
Тест из 5 вопросов: когда правило идёт в hook, а когда остаётся в CLAUDE.md?
У меня это работает как закон, а не как пожелание. Я для себя свёл выбор между CLAUDE.md и хуком к простому тесту из пяти вопросов. Если хотя бы три ответа «да» - правило идёт в хук. Меньше трёх - остаётся в тексте.
Тест «когда правило идёт в hook»:
- Это правило про действие, а не про контекст? («не запускай
rm -rf» - действие; «у меня TypeScript-проект» - контекст) - Это правило про безопасность, деньги или данные? («не трогай
.env», «не публикуй ключи») - Можно проверить через grep, regex или exit code shell-команды? («после Edit
.mdпроверь что нет длинного тире») - Если правило нарушится, последствия будут болезненно отменять? («force push в main» - сложно откатить; «забыл точку в конце» - легко)
- Я уже видел, как Claude забывал это правило хотя бы один раз?
Пример: «не используй длинное тире, только дефис». Действие? Да. Безопасность? Нет. Проверяется grep? Да. Сложно отменить? Нет. Видел нарушение? Да. 3 из 5 - в хук.
Контр-пример: «у меня вайб-кодеры, не разработчики, пиши без слэнга». Действие? Скорее контекст. Безопасность? Нет. Проверяется регексом? Слишком сложно. Сложно отменить? Нет. Видел нарушение? Да. 1 из 5 - в CLAUDE.md.
Через тест прошли 5 моих правил. Они переехали в хуки. Остальные 75% - остались в текстовом файле.
7 готовых хуков под копипаст
Все 7 хуков хранятся в одном файле .claude/settings.json в корне проекта. Каждый - запись с типом события, фильтром по инструменту (matcher) и shell-командой. Если команда возвращает exit code 2 - действие модели блокируется, и Claude получает stderr как сообщение о причине.
For most hook events, only exit code 2 blocks the action. Claude Code treats exit code 1 as a non-blocking error and proceeds with the action, even though 1 is the conventional Unix failure code.
exit 0 - прошло. exit 1 - ошибка, но Claude продолжит (это первый антипаттерн ниже). exit 2 - жёсткий стоп, расскажи Клоду что не так.
Хук 1. Защита .env от любых правок (PreToolUse, exit 2)
Блокирует попытку отредактировать любой файл вида .env, .env.local, .env.production напрямую.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Запрещено редактировать .env напрямую. Скажи мне какую переменную добавить, я сам открою файл.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}Что делает: перед каждым Edit/Write/MultiEdit берёт путь файла, проверяет grep'ом совпадает ли он с шаблоном .env, и если да - возвращает exit 2 с сообщением. Claude видит stderr и пишет мне: «попытался отредактировать .env, хук заблокировал, говори какую переменную добавить».
Хук 2. Авто-prettier после каждой правки кода (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
}
]
}
]
}
}Что делает: после каждого Edit/Write берёт путь, если расширение .ts/.tsx/.js/.jsx/.json - запускает prettier в фоне через &. exit 0 всегда, потому что это форматирование, а не запрет. Если prettier упадёт - ничего страшного, файл останется как был.
Разница с хуком 1 принципиальна: PreToolUse + exit 2 - жёсткий блок ДО действия. PostToolUse + exit 0 - мягкая правка ПОСЛЕ действия. Это два разных режима, под разные задачи.
Хук 3. Запрет git push --force в ветку main (PreToolUse, exit 2)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push в main запрещён. Сделай новую ветку или попроси меня вручную.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}Что делает: на каждый запуск Bash проверяет, что в команде есть push, есть --force/--force-with-lease/-f, и упоминается main. Если все три условия совпали - блок с понятным сообщением.
Раньше у меня в CLAUDE.md это правило было заглавными буквами. Claude всё равно нарушал примерно раз в две недели, когда я просил «зафорсись чтобы откатить мой неудачный коммит». Теперь это физически невозможно.
Хук 4. Блокировка длинного тире в текстовых файлах (PostToolUse, exit 2)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"В файле $FILE найдено длинное тире (U+2014). Замени на обычный дефис (U+002D).\" >&2; exit 2; } || exit 0"
}
]
}
]
}
}Что делает: PostToolUse с exit 2 - это пост-проверка с блокировкой результата. Если Claude отредактировал файл и оставил длинное тире - хук падает, Claude получает обратную связь и пробует снова. Через 2-3 сессии Claude перестаёт ставить этот символ вообще.
Обрати внимание: grep ищет именно символ длинного тире (U+2014) через $'\xe2\x80\x94' - bash-форма записи через UTF-8 байты. Если просто написать grep -q '-' - будет ловить обычный дефис и срабатывать на каждом файле. Невидимый символ ломает всю логику.
Хук 5. Освободить порт 3000 перед npm run dev (PreToolUse, exit 0)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Порт 3000 освобождён перед запуском' >&2; }; exit 0"
}
]
}
]
}
}Что делает: перед каждым npm run dev (или pnpm/yarn) убивает процесс на порту 3000. exit 0 всегда, потому что это подготовка, а не запрет. Защищает от класса ловушек, когда Claude видит «порт занят», думает что проблема в коде и начинает чинить - а на деле просто старый сервер не закрылся.
Хук 6. Авто-коммит после успешных тестов (PostToolUse, exit 0)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command // empty'); EXIT=$(jq -r '.tool_response.exit_code // 1'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+(run[[:space:]]+)?test' && [ \"$EXIT\" = \"0\" ] && { git add -A && git commit -m 'tests: автокоммит после успешного прогона' --no-verify 2>/dev/null; }; exit 0"
}
]
}
]
}
}Что делает: после каждого npm test / pnpm test / yarn test проверяет код возврата. Если exit_code = 0 (тесты прошли) - делает git add -A и коммит с понятным сообщением. exit 0 всегда. Чекпоинт автоматически - не нужно помнить про коммит.
Антипаттерн: ставить git commit --no-verify на каждый Edit. Это создаёт мусорные коммиты после каждой строчки кода. --no-verify нужен только потому что pre-commit хуки сами часто запускают тесты, и получится бесконечный цикл. Если у тебя нет pre-commit - убери --no-verify.
Хук 7. Инжект свежего контекста при старте сессии (SessionStart с additionalContext)
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Текущая ветка: $BRANCH. Незакоммиченные изменения:\\n$STATUS\\\"}\""
}
]
}
]
}
}Что делает: при старте сессии собирает текущую git-ветку и список незакоммиченных изменений, возвращает JSON с полем additionalContext. Claude Code инжектит этот текст в первое сообщение системы - Claude знает с самого начала, где он находится, что осталось от прошлой сессии.
Use additionalContext for information Claude should know about the current state of your environment or the operation that just ran: Environment state (the current branch, deployment target, or active feature flags), Conditional project rules (which test command applies to the file just edited), External data (open issues assigned to you, recent CI results).
Расширения этого хука: подтянуть последний коммит и его описание, статус GitHub Actions, активный спринт из Linear/Jira, открытые issues по текущему репозиторию.
3 ловушки хуков, в которые попадает каждый второй
Ловушка 1. Хук с exit 1 молча проходит
Самая опасная. Я неделю думал, что мой .env-protector работает, а оказалось - у меня в хуке был jq -r '.tool_input.file_path' без проверки на null. Когда tool_input был пустой, jq падал с exit 1. Claude видел exit 1 как «ошибка хука, продолжаем» - действие проходило. Долго не понимал, почему .env всё-таки трогается. Только когда вручную запустил хук в терминале с пустым входом - увидел причину.
Лечится одним правилом: в каждом хуке начинай с проверки на null, и в конце ставь || exit 0. Любой неожиданный выход даст «безопасный» exit, а не молчаливое прохождение.
# Шаблон безопасного начала и конца:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... твоя логика ...
exit 0 # или || exit 0 в одну строкуЛовушка 2. Медленные хуки убиваются по таймауту
У меня был хук, который запускал eslint --fix на весь файл после Edit. Большие файлы (1000+ строк) обрабатывались по 8-10 секунд. Стандартный таймаут хука в Claude Code - 60 секунд, я в него вроде вписывался. Но когда несколько Edit идут подряд за минуту - хуки накапливаются, стоят в очереди, и часть просто пропускается. Понял это, только когда в логе вышло «hook timed out».
Лечится так: если хук требует тяжёлой работы, выноси в фон. eslint --fix "$FILE" & без ожидания результата. Тогда хук возвращает exit 0 за миллисекунды, а реальная работа крутится в background.
# Тяжёлая операция в background:
npx eslint --fix "$FILE" &
exit 0Ловушка 3. Хук не видит переменные окружения проекта
Я думал, что process.env в хуке доступен как в Node.js-приложении. Нет. Хук запускается под shell от родителя Claude Code и видит только то, что есть в ~/.zshrc или ~/.bashrc, плюс то, что Claude Code пробрасывает явно. Если твой хук зависит от OPENAI_API_KEY из .env - придётся читать .env вручную.
Лечится: явное чтение .env в команде хука.
# Прокидывание env проекта в hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Или через dotenv:
dotenv -e .env -- node my-validator.jsОсобенно больно в монорепо, где разные пакеты имеют свои .env. Решается прокидыванием PWD и явным cd "$PWD" в начале команды.
Как тестировать хуки до коммита?
Базовый сценарий теста. Берёшь команду хука из JSON, открываешь терминал и подаёшь ей на stdin тот самый JSON, который Claude Code прислал бы:
# Тест хука 1 (защита .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
echo "BLOCKED" >&2
exit 2
} || exit 0
'
echo "Exit code: $?"Ожидаешь BLOCKED в stderr и Exit code: 2. Если получил exit 0 - твой регекс не работает, чини grep.
Сценарии для проверки:
- Happy path - файл должен заблокироваться.
.envдля хука 1,git push --force mainдля хука 3. - Negative path - файл не должен заблокироваться.
README.mdдля хука 1,git push origin feature-branchдля хука 3. - Edge cases - пустой input, null значения, неожиданные символы. Хук должен либо ответить exit 0, либо упасть с понятной ошибкой - не молча проходить.
Где складывать экспериментальные хуки. Не в .claude/settings.json, а в .claude/settings.local.json (не в git). Туда складываешь новые хуки в PostToolUse с echo вместо реального действия, смотришь срабатывают ли вообще, потом переписываешь в боевой settings.json.
// Эксперимент в settings.local.json - пишет в /tmp/hook-debug.log:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
}
]
}
]
}
}После 5-10 правок в Claude Code открываешь /tmp/hook-debug.log - видишь, на каких файлах хук сработал. Если пусто - matcher не подходит, чини регекс.
Что дальше?
5 шагов на ближайшую неделю:
- Прогрепай свой
CLAUDE.mdпо словам «никогда», «запрещено», «всегда», «обязательно». Каждое такое правило - кандидат в хук. Если правило императивное, текст в файле его не удержит. - Начинай с PostToolUse + exit 0. Это самые безопасные - что-то поправляют, ничего не блокируют. Prettier (хук 2), убийство dev-сервера (хук 5), авто-коммит после тестов (хук 6).
- Только когда уверен - добавь первый PreToolUse + exit 2. Например, защиту
.env(хук 1) или запрет force-push (хук 3). Это уже жёсткий блок, ошибиться в команде grep = заблокировать самого себя. - Тестируй хуки вручную перед коммитом (раздел выше). Так экономишь часы отладки.
- Держи
settings.local.jsonдля экспериментов. Туда новые хуки с echo вместо реального действия.
После того как хуки настроены - следующий шаг кластера: как откатить изменения в Claude Code, если хук всё-таки пропустил что-то опасное. Если хочешь, чтобы Claude помнил структуру проекта между сессиями - собери Второй мозг в Claude Code (папка business/ + связанная база знаний). А чтобы навыки твоего проекта были именованы и переиспользуемы - Claude Code Skills.
Источники
- Claude Code Hooks reference - официальная документация Anthropic
- Claude Code Memory documentation - про CLAUDE.md и системный промпт
- Automate workflows with hooks - официальный гайд Anthropic с примерами
- Claude Code Hooks: Complete Guide to All 12 Lifecycle Events - claudefa.st
- Claude Code Hooks: 6 Production Patterns - pixelmojo.io
- Claude Code Hooks Tutorial: 5 Production Hooks From Scratch - Blake Crosley
- Claude Code Hooks: A Practical Guide to Workflow Automation - DataCamp
- Claude Code Hooks: 20+ Ready-to-Use Examples - DEV.to / Lukasz Fryc
- Claude Code Hooks Mastery - disler/claude-code-hooks-mastery (GitHub)
- Claude Code: маршрут обучения 2026 - Хабр
- Claude Code: практический гайд по настройке - Хабр
- 44 настройки Claude Code - Хабр
Полная схема по вайб-кодингу за вечер: ИИ-клон + Второй мозг + Контекст-инжиниринг. 3 эфира, 2 000 ₽. Записи остаются у тебя.

