Hooks - это место, где инструкция превращается в механизм. Они запускаются на событиях жизненного цикла и бывают двух видов: команда, которая получает событие в виде JSON на вход и возвращает решение на выход, и обработчик на основе запроса к модели. Разница с правилами принципиальна: правило советует, hook исполняется всегда и одинаково. Правило можно перевесить другим текстом, более срочным или более подробным, - командный hook перевесить нечем, он не участвует в рассуждении. У обработчика на основе запроса к модели этой гарантии нет: он сам выносит суждение и потому ближе к правилу, чем к механизму. Именно поэтому им доверяют то, что должно работать независимо от формулировок.
Семантика кодов возврата у команды простая, и её надо знать наизусть. Ноль означает успех. Двойка блокирует действие. Все остальные коды по умолчанию не блокируют - поведение fail-open, то есть при сбое обработчика работа продолжается. Такой выбор по умолчанию сделан осознанно: сломанный обработчик не должен парализовать редактор у всей команды. Для критичных проверок есть отдельный флаг, переводящий hook в режим fail-closed: если проверка не отработала, действие не выполняется.
Выбор между этими двумя режимами - главное проектное решение при работе с hooks. Логирующий hook должен переживать недоступность внешней системы: если сборщик логов упал, разработка не должна останавливаться, поэтому здесь fail-open разумен. Политический hook, который ловит секреты или разрушительные команды, должен быть fail-closed: непроверенное действие лучше не выполнить, чем выполнить вслепую. Смешивать эти роли в одном обработчике - верный способ получить и остановки на пустом месте, и дыры одновременно: одна половина работы требует терпимости к сбоям, вторая её категорически не допускает.
Полезно один раз увидеть определение целиком. Ниже - hook на событие перед выполнением команды оболочки: тип, путь к обработчику, таймаут и явный режим fail-closed. Таймаут здесь не формальность: обработчик, который висит дольше, чем человек готов ждать, превращает защиту в раздражение, а раздражение рано или поздно превращается в отключённую защиту. Отсюда практическое требование к политическому обработчику: проверка должна быть локальной и быстрой, без сетевых обращений, потому что сеть - это и есть источник непредсказуемой задержки.
Как ошибка в режиме отказа выглядит вживую, стоит представить заранее. Обработчик, который отправляет записи аудита во внешнюю систему, поставили в режим fail-closed, чтобы ни одно действие не осталось незаписанным. Через месяц внешняя система уходит на обслуживание, и вся команда обнаруживает, что агент перестал выполнять любые команды. Обратный случай тише и хуже: обработчик, ищущий секреты в правках, оставили в fail-open, он падает на первой же строке из-за опечатки в пути, и полгода никто этого не замечает, потому что снаружи всё выглядит как раньше. Тихий отказ защиты - самый дорогой из двух.
Источники hooks имеют строгий приоритет: корпоративный уровень сильнее командного, командный сильнее проектного, проектный сильнее пользовательского. Проектные пути выполняются от корня проекта. Из этого следует практическое правило: обязательные проверки живут наверху, где их нельзя снять локально, а удобные - внизу, где их легко поправить под себя. Проверка, которую человек может отключить в своём файле за десять секунд, политикой не является, как бы строго она ни была написана.
И главное предупреждение: hook - это исполняемый код, приехавший вместе с репозиторием. Доверять ему только потому, что он лежит в проекте, нельзя. Его читают как код: что запускает, какие зависимости тянет, что печатает в вывод, не уходит ли содержимое наружу. Закреплять версии зависимостей и ограничивать вывод здесь так же важно, как в любом другом коде, который вы согласились выполнять. Разница лишь в том, что этот код запускается на каждое событие и без отдельного подтверждения, то есть внимания заслуживает больше обычного, а не меньше.
Полезно понимать границу между hook и списком разрешённых команд, потому что задачи у них соседние. Список разрешённого отвечает на вопрос, знакома ли команда: сравнение идёт с текстом, решение бинарное, настройка дешёвая. Hook отвечает на вопрос, допустима ли она в этих обстоятельствах: он видит событие целиком и может учесть ветку, каталог, содержимое правки, время суток. За эту выразительность платят кодом, который надо писать, ревьюить и поддерживать. Правило выбора простое: если требование выражается перечислением, его выражают перечислением, а hook берут тогда, когда решение зависит от контекста.
Инженерный вывод простой: hooks дают детерминированность там, где суждение модели ненадёжно. Обязательный линт после правки, запрет разрушительной команды, запись аудита - всё это исполнится одинаково при любой формулировке запроса. Но за эту надёжность платят вниманием к деталям: таймаут, режим отказа, права процесса и ревью самого обработчика. И платят проверкой: hook, для которого не написан негативный сценарий, считается неработающим, пока не доказано обратное.
Типичные провалы предсказуемы. Смешать аудит и политику в одном обработчике. Оставить критичную проверку в режиме fail-open и не заметить, что она годами не срабатывала. Написать hook без таймаута и получить зависание вместо защиты. Поставить сетевой вызов в путь, который обязан отработать за секунды. И принять hook из репозитория как доверенный, не прочитав его.
// .cursor/hooks.json
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"type": "command",
"command": ".cursor/hooks/check-command.sh",
"timeout": 10,
"failClosed": true
}
]
}
}
// приоритет источников: корпоративный, командный, проектный, пользовательский