Что такое хук и зачем он
Обычно агент делает только то, что ты попросил в переписке. Хук — это способ сказать ему заранее: «в такой-то момент выполни вот этот мой скрипт». Агент запускает его сам, без напоминаний.
Ключевое отличие от промпта: хук выполняется всегда. Промпт можно забыть написать, хук — нет. Поэтому в хуки уходит всё, что должно происходить каждый раз и не должно зависеть от твоей памяти.
Почему это главное — про запись в моменте
Обычный сценарий без хуков выглядит так. Ты полчаса что-то обсуждал с агентом, выяснил три важные вещи, принял решение — и закрыл вкладку. Или сессия упёрлась в лимит. Или ты просто переключился на другое. Всё, что выяснилось, осталось в переписке, которую ты больше не откроешь.
Причина в том, что запись обычно привязана к двум условиям: ты должен о ней попросить и ты должен довести сессию до конца. Оба условия ненадёжны — именно в интересных сессиях о них забывают.
Хуки снимают оба. Событие Stop срабатывает после каждого ответа агента, а не в конце сессии — то есть точек сохранения за один разговор десятки. Плюс PostToolUse после каждого действия и PreCompact перед тем, как сжатие контекста что-то потеряет. Записи копятся по ходу, сами.
Для второго мозга это решающая деталь. База знаний, которую надо наполнять вручную, не наполняется — проверено на себе. Наполняется та, которая пишется сама, пока ты занят разговором.
Три типовые задачи
- Писать по ходу. Решения, находки, цифры и контекст оседают в файлах прямо во время работы — без просьбы и до того, как сессия закончилась.
- Дать контекст на входе. Агент открывает сессию и уже знает: над чем работали вчера, какие проекты живы, где что лежит. Не нужно пересказывать.
- Поставить границу. Перед выполнением команды проверить, не делает ли он чего-то, чего делать нельзя, — и заблокировать.
Какие события бывают
Событий много, но в реальной работе нужны единицы. Вот те, ради которых стоит открывать конфиг:
| Событие | Когда срабатывает | Что может |
|---|---|---|
SessionStart | Сессия открывается или продолжается | Вывод уходит в контекст — так дают память на старте |
UserPromptSubmit | Ты отправил сообщение, до обработки | Вывод тоже уходит в контекст — подмешать данные под конкретный запрос |
PreToolUse | Перед вызовом инструмента | Заблокировать вызов — стоп-кран на опасные команды |
PostToolUse | После успешного вызова | Инструмент уже отработал; можно показать агенту предупреждение |
Stop | После каждого ответа агента, а не в конце сессии | Главная точка авто-сохранения: десятки срабатываний за разговор. Умеет и не дать агенту закончить |
SubagentStop | Субагент закончил | То же, но для подзадач |
PreCompact | Перед сжатием контекста | Успеть сохранить то, что сжатие потеряет |
SessionEnd | Сессия завершается | Прибраться за собой |
Есть ещё пара десятков событий — на смену рабочей папки, на изменение файла на диске, на уведомления, на запрос прав. Они нужны редко; начинать стоит с SessionStart и Stop.
Как настроить
Хуки живут в settings.json. Глобально для всех проектов — ~/.claude/settings.json; для одного проекта — .claude/settings.json в его корне.
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/context.sh",
"timeout": 20
}
]
}
]
}
}Что здесь важно. matcher — фильтр, когда именно срабатывать. Для SessionStart значения такие: startup (свежий запуск), resume (продолжение), clear (после очистки), compact (после сжатия контекста), fork (после форка сессии).
matcher. Без него хук срабатывает на все события сразу, включая compact. Получается петля: контекст переполнился → произошло сжатие → хук снова влил свой объём → ты быстрее приехал к следующему сжатию. Для памяти на старте почти всегда нужен именно "startup".Для хуков на инструменты (PreToolUse, PostToolUse) matcher фильтрует по имени инструмента: "Bash", "Edit|Write", "mcp__.*" — поддерживается и список через |, и регулярка, и "*" на всё.
Коды возврата
| Код | Что значит |
|---|---|
0 | Успех. Вывод уходит в контекст (для событий, которые это умеют) |
2 | Блокирующая ошибка — действие отменяется. Так работает стоп-кран |
| любой другой | Ошибка без блокировки: действие продолжится, первая строка stderr покажется в транскрипте |
Хук получает на вход JSON: session_id, cwd, hook_event_name, transcript_path, а для событий на инструменты ещё tool_name и tool_input. Это и есть материал для проверок.
Пример 1. Память по проектам и сайтам на старте
Мой основной хук. Задача: агент открывает сессию в любой папке и уже знает контур — какие проекты живы, какие сайты крутятся, где что лежит.
Главный принцип, к которому я пришёл не сразу: в хук идут карты, а не данные. Первая версия грузила канбан проектов целиком — 97 КБ, это 32 500 токенов в каждой сессии. Заменил на номенклатурную карту в 2 КБ: имя проекта → строка сути → где лежат детали. Агент по-прежнему знает все проекты по именам и сам открывает нужную карточку грепом, когда до неё дошло дело.
#!/usr/bin/env bash
# ⚠️ БЮДЖЕТ: суммарно ≤ 25 КБ (~8k токенов).
# Вывод уезжает в контекст и пересылается КАЖДЫЙ ход каждой сессии.
# Один лишний файл здесь дороже десяти чтений по запросу.
#
# Сюда идут КАРТЫ, а не данные:
# ✓ где я остановился (2-3 КБ) ✓ номенклатура проектов ✓ карта хранилища
# ✗ канбан целиком ✗ справочники ✗ логи ✗ личное (хук работает в ЛЮБОЙ папке)
set -euo pipefail
BASE="$HOME/brain"
[ -d "$BASE" ] || exit 0 # нет хранилища — молча выходим, не роняем сессию
files=(
"_BRAIN/hot.md" # где я остановился в прошлый раз
"_BRAIN/PROJECTS_map.md" # имя проекта → суть → где детали
"_BRAIN/INDEX.md" # карта хранилища
)
echo "# Рабочий контекст (автозагрузка)"
echo
echo "Это КАРТЫ, а не данные: детали открывать по запросу, грепом по \`$BASE\`."
echo "В конце содержательной сессии обнови _BRAIN/hot.md — 3-6 строк, не больше."
echo
for f in "${files[@]}"; do
[ -f "$BASE/$f" ] || continue
echo "---"; echo "## $f"; echo
cat "$BASE/$f"; echo
doneПро сайты отдельно. У меня их 15, и держать их состояние в голове агента бессмысленно — оно меняется каждый день. Поэтому в карту идёт только строка на сайт (домен, что это, где репозиторий, как деплоится), а живые цифры — трафик, что опубликовано, что сломалось — агент достаёт по запросу из отдельных файлов. В хуке этого нет вообще.
Пример 2. Запись контекста по ходу сессии
Тот самый главный сценарий. Проблема: сессия оборвалась — и всё, что в ней выяснилось, растворилось. Следующая начинается с чистого листа, ты пересказываешь заново.
Важно понимать механику: Stop срабатывает после каждого ответа агента. То есть это не «сохранение на выходе», а регулярная точка записи в течение всего разговора — сколько ответов, столько и шансов зафиксировать. Даже если ты закроешь вкладку на середине, последние срабатывания уже отработали.
Решение — resume-кэш: маленький файл, в который агент пишет в конце «над чем работали, следующий шаг, какие файлы тронуты». На старте этот же файл подхватывается хуком из примера 1. Круг замыкается.
Ключевая деталь, на которой я обжёгся: у такого файла обязан быть лимит. Мой разросся с обещанных в его же шапке «3–6 строк» до 22 КБ и девяти накопленных сессий — то есть тихо превратился в ещё один справочник в автозагрузке. Лимит пишется и в сам файл, и в правила.
#!/usr/bin/env bash
# Срабатывает, когда агент закончил отвечать.
# Не пишет файл сам — напоминает агенту это сделать, если сессия была содержательной.
set -euo pipefail
HOT="$HOME/brain/_BRAIN/hot.md"
[ -f "$HOT" ] || exit 0
# Если файл не трогали больше двух часов, а работа шла — пора обновить
LAST=$(( ($(date +%s) - $(stat -f %m "$HOT" 2>/dev/null || echo 0)) / 60 ))
[ "$LAST" -lt 120 ] && exit 0
cat <<'TXT'
Прежде чем закончить: если в этой сессии была содержательная работа —
обнови _BRAIN/hot.md (3-6 строк: над чем работали, следующий шаг, тронутые файлы).
Лимит файла — 2 последние сессии, старое выкидывай. Если работы не было — не трогай.
TXTStop может не дать агенту закончить и заставить продолжить работу. Это мощно и легко превращается в бесконечный цикл. Поэтому в примере выше стоит проверка по времени — она не даёт хуку срабатывать на каждый чих.Пример 3. Как у меня собираются решения
Самая недооценённая вещь. В переписке постоянно принимаются решения: выбрали стек, отказались от канала, поменяли цену, решили что-то не делать. Через месяц ты не помнишь ни решения, ни аргументов — и обсуждаешь заново.
Я веду журнал решений: один файл на решение, формат жёсткий — контекст, само решение, почему так, последствия. Пишет их агент, сам, без напоминания — потому что это записано в правилах как триггер:
## Триггеры записи (без спроса)
- В диалоге появилось решение, на которое сошлются позже (выбор стека, отказ
от канала, смена позиционирования, принятие/отказ оффера) → завести файл
решения: контекст / решение / почему так / последствия. 5-10 строк, не больше.
- Факт, цифра, метод, рабочий конфиг, результат ресёрча, который не хочется
искать заново → короткая заметка с тегами в первой строке.
- Не сохранять: общие обсуждения, легко восстановимые ответы, дубли.
Тема уже есть — дополни существующее, не плоди второй файл.Почему это правило, а не хук: решение — вещь смысловая, скрипт его не распознает. Хук здесь работает с другой стороны — на старте подаёт агенту карту хранилища, чтобы он знал, куда это класть и что там уже есть. Без карты он либо не запишет, либо создаст пятый файл на ту же тему.
Это общий принцип: хук отвечает за то, чтобы нужное было под рукой; правило — за то, чтобы им воспользовались. Порознь оба работают плохо.
Пример 4. Стоп-кран на опасные команды
Единственный хук, который не про контекст, а про безопасность. Проверяет команду до выполнения и блокирует, если она из чёрного списка. Код возврата 2 — это и есть «заблокировать».
#!/usr/bin/env bash
# Блокирует опасные команды до выполнения. Выход 2 = заблокировать.
set -euo pipefail
CMD=$(cat | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))")
deny() { echo "Заблокировано хуком: $1" >&2; exit 2; }
case "$CMD" in
*"rm -rf /"*) deny "удаление от корня" ;;
*"git push --force"*) deny "force-push (используй --force-with-lease)" ;;
*"git reset --hard origin"*) deny "сброс локальных правок" ;;
*"DROP TABLE"*|*"TRUNCATE"*) deny "разрушающий SQL" ;;
*".env"*"rm "*) deny "удаление .env" ;;
esac
exit 0{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/guard.sh" }]
}
]
}
}Бюджет хука — правило, которое сэкономит тебе больше всего
Всё, что печатает SessionStart, пересылается заново каждый ход. Не один раз за сессию — каждый. Поэтому у хука должен быть явный потолок, записанный прямо в шапке скрипта, чтобы будущий ты его видел.
Мой ориентир: не больше 25 КБ вывода, то есть примерно 8 тысяч токенов. Как проверить свой:
python3 -c "
import json,os,subprocess
s=json.load(open(os.path.expanduser('~/.claude/settings.json')))
for grp in s.get('hooks',{}).get('SessionStart',[]):
for h in grp.get('hooks',[]):
out=subprocess.run(h['command'],shell=True,capture_output=True).stdout
print(len(out),'байт ~',len(out)//3,'токенов <-',h['command'])
"Полный разбор, куда ещё утекает контекст и как срезать его в разы, — в гайде про экономию токенов.
Грабли
- Забыл matcher — хук бьёт по всем событиям
Включая
compact. Контекст сжался, хук снова влил свой объём, ты быстрее приехал к следующему сжатию. Для памяти на старте ставь"startup". - Хук молча растёт Добавил файл, ещё файл, ещё — и через полгода стартовый вес сессии в три раза больше. Лечится потолком в шапке скрипта и периодическим замером.
- Личное в глобальном хуке Глобальный хук работает в любой папке — и на демонстрациях, и на записи экрана. Финансы, семья, здоровье в него попадать не должны: такие файлы читаются по запросу.
- Хук роняет сессию
Скрипт упал — старт сломался. Всегда предусматривай тихий выход: нет файла или папки —
exit 0, а не ошибка. И ставьtimeout. - Stop-хук зацикливается
Stopумеет не дать агенту закончить. Без условия выхода получается вечный цикл. Ставь проверку — по времени, по флагу, по счётчику. - Данные вместо карт Самая дорогая ошибка. Справочник в автозагрузке едет с тобой каждый ход; тот же справочник, открытый грепом по запросу, — один раз и по делу.
Промпты — копировать и вставлять
Собери мне SessionStart-хук, который на старте сессии даёт тебе рабочий контекст.
Условия:
- Вывод — только КАРТЫ и указатели, не данные. Потолок 25 КБ, запиши его в шапку скрипта.
- Если файла или папки нет — тихий выход (exit 0), сессия не должна падать.
- Личное (финансы, семья, здоровье) в вывод не включать: хук работает в любой папке.
- Подключи через matcher "startup", чтобы не срабатывал после сжатия контекста.
Сначала посмотри, что у меня вообще есть по проектам и заметкам, предложи состав
вывода списком и посчитай примерный вес. Скрипт пиши после того, как я подтвержу состав.Проверь мои хуки в ~/.claude/settings.json и в .claude/settings.json проекта:
1. Для каждого — что запускает, сколько байт выводит, сколько это токенов.
2. У каких SessionStart-хуков не задан matcher (значит, бьют и по compact)?
3. Что из выводимого — данные, а не карты? Для каждого предложи замену указателем.
4. Есть ли риск уронить сессию: нет тихого выхода, нет timeout?
5. Не утекает ли личное в глобальный хук?
Дай таблицу находок и план правок. Ничего не меняй, пока не подтвержу.Собери PreToolUse-хук на Bash, который блокирует опасные команды (выход 2).
Посмотри, с чем я реально работаю — какие тут репозитории, серверы, базы —
и предложи чёрный список под МОЙ контур, а не общий шаблон. Для каждого пункта
объясни, какой сценарий он предотвращает.
Отдельно скажи, что блокировать НЕ надо, потому что это сломает нормальную работу.Дальше — про то, куда утекает контекст
Хук — только один из источников. Полный разбор с цифрами: стартовый вес сессии, субагенты, выбор модели, готовые правила для CLAUDE.md и AGENTS.md.
Читать дальше
Все гайды рабочие: каждый — то, что реально крутится у меня, а не пересказ документации.