Hooks встраивают командные скрипты прямо в agentic loop: они реагируют на события жизненного цикла и могут разрешить, изменить или заблокировать действие. Codex читает hooks.json и inline-секции [hooks] рядом с активными слоями конфигурации, и совпавшие hooks из разных источников выполняются вместе. Как и остальная исполняемая конфигурация, project hooks действуют только в доверенном проекте - это первая линия защиты от чужого кода, приехавшего с репозиторием.
Полезно один раз увидеть определение hook целиком. Ниже - hooks.json с событием PreToolUse, matcher по имени инструмента "^Bash$", командой-обработчиком и таймаутом. Matcher важен: он задаёт, на какие именно действия срабатывает hook, - здесь на попытки выполнить Bash. Именно связка события и matcher делает hook точечным инструментом, реагирующим на конкретный класс действий, а не на всё подряд.
Доверие к hook привязано к хэшу его определения, и это ключевой механизм безопасности. Non-managed hook требует ревью точного определения; при изменении команды его статус возвращается к "needs review". То есть изменённый hook не начинает исполняться молча - он снова становится недоверенным, пока вы явно не подтвердили его новое содержимое. Это защита от тихой подмены: правка hook кем-то другим не проходит незаметно мимо вашего внимания.
Управляют доверием и состоянием hooks через отдельную команду. /hooks показывает источник каждого hook, его trust и включён ли он. Это снимает неопределённость: вместо предположения о том, какие hooks в игре и кто их одобрил, вы видите список с источниками и статусами. Отдельно есть флаг --dangerously-bypass-hook-trust, но у него узкое место применения - только автоматизация, где источник проверяется до запуска, а не интерактивный обход неудобной проверки.
Обработчик hook устроен по простому протоколу, который надо соблюдать. Он получает JSON на stdin и сообщает решение через протокол exit-кодов и статусов, описанный в официальном руководстве. Полезно один раз увидеть это в связке с определением. Обработчик - это обычная программа с правами процесса Codex, поэтому его пишут как код, за который отвечают: с понятным входом, явным решением и предсказуемым поведением, а не как одноразовый скрипт наугад.
Три правила безопасного обработчика важнее прочих. Не печатайте секреты - вывод hook может попасть в контекст или логи. Не делайте hook молча деструктивным - действие с последствиями должно быть явным и ожидаемым, а не спрятанным внутри проверки. И держите таймаут короче пользовательского терпения: hook, который висит дольше, чем человек готов ждать, превращает защиту в раздражение. Failure mode при этом должен быть понятным, а не загадочным зависанием.
Смысл hooks - в детерминированной проверке там, где нельзя полагаться на суждение модели. Обязательный lint после правки, запрет опасной команды, проверка политики перед действием - всё это hook исполнит одинаково всегда, в отличие от модели, которая может по-разному отнестись к одной ситуации. Но именно потому, что hook - исполняемый код с правами процесса, его доверие, таймаут и failure mode проектируют так же тщательно, как саму проверку.
Типичные провалы вокруг hooks предсказуемы. Написать слишком широкий matcher и срабатывать не на том. Забыть, что изменение команды возвращает hook в "needs review", и удивиться, что он не работает. Применить --dangerously-bypass-hook-trust в интерактиве вместо узкой автоматизации. И написать обработчик, который печатает секреты, молча деструктивен или висит дольше терпения. Задавайте точные matcher, ревьюйте по хэшу, пишите обработчик как код и держите таймаут и failure mode понятными.
{
"description": "Repository policy checks",
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"hooks": [
{ "type": "command",
"command": "node .codex/hooks/check-command.mjs",
"timeout": 10 }
]
}
]
}
}
// trust привязан к hash; изменение команды -> "needs review"; /hooks показывает source/trust/enable