Модуль 2 · Расширения · урок 13 из 16
Хуки: проверки и автоматизация вокруг агента
AGENTS.md и навыки — это просьбы: модель их обычно выполняет, но гарантии нет. Хуки (hooks) — другое: это ваш код, который Codex запускает сам в определённые моменты работы агента. Хук может запретить команду, подмешать контекст, проверить результат и заставить агента продолжить работу. Хуки в Codex вышли из экспериментального статуса в мае 2026 года и включены по умолчанию.
События
| Когда | События |
|---|---|
| Старт сессии или субагента | SessionStart, SubagentStart |
| Во время хода | UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, SubagentStop, Stop |
| Прерывание хода | Interrupt |
| Конец основной сессии | SessionEnd |
Самые полезные на практике:
PreToolUse— до вызова инструмента: команды оболочки (матчерBash), правки файлов (apply_patch, а такжеEditилиWrite), MCP-инструменты (mcp__сервер__инструмент). Может запретить вызов или переписать его аргументы.PostToolUse— после вызова: проверить вывод и дать агенту обратную связь. Отменить уже выполненную команду не может.Stop— когда агент собирается закончить ход. Может вернуть его к работе с новым заданием.SessionStart— при старте, возобновлении, очистке, компакции или ответвлении сессии. Всё, что хук напечатает, попадает в контекст как дополнительная инструкция разработчика.
Встроенные серверные инструменты вроде веб-поиска хуки не видят — они не проходят через локальный путь вызова инструментов.
Где лежат и как выглядят
Codex ищет хуки рядом с каждым активным слоем конфигурации: в файле hooks.json или в таблицах [hooks] внутри config.toml. Чаще всего это:
~/.codex/hooks.jsonили~/.codex/config.toml— личные;<repo>/.codex/hooks.jsonили<repo>/.codex/config.toml— проектные, загружаются только для доверенных проектов.
Хуки из всех источников складываются, а не заменяют друг друга. В одном слое лучше держать что-то одно — hooks.json или [hooks].
Структура трёхуровневая: событие → группа с матчером → список обработчиков. matcher — регулярное выражение по имени инструмента (для SessionStart — по источнику старта). У UserPromptSubmit и Stop матчера нет.
{
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"hooks": [
{
"type": "command",
"command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/guard_bash.py\"",
"timeout": 10,
"statusMessage": "Проверяю команду"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/php_lint_on_stop.py\"",
"timeout": 60
}
]
}
]
}
}Путь к скрипту строится от корня git: Codex запускает команду в рабочей папке сессии, а сессию могут открыть из подкаталога. timeout — в секундах, по умолчанию 600. Кроме "type": "command" есть "type": "mcp_tool" — вызов инструмента уже подключённого MCP-сервера.
Хук 1. Запрет опасных команд
Каждый командный хук получает на stdin JSON с полями события. Для PreToolUse это, среди прочего, tool_name и tool_input; у команд оболочки текст лежит в tool_input.command.
#!/usr/bin/env python3
"""PreToolUse: не даёт агенту запускать разрушительные команды."""
import json
import re
import sys
event = json.load(sys.stdin)
command = event.get("tool_input", {}).get("command", "")
if isinstance(command, list):
command = " ".join(command)
BLOCKED = [
(r"\bgit\s+push\b.*(\s--force(?!-)|\s-f(\s|$))",
"Force push запрещён. Сделайте обычный push или попросите человека."),
(r"\bmigrate:(fresh|reset)\b",
"Команда пересоздаёт базу. Такие операции выполняет человек."),
(r"\bDROP\s+(TABLE|DATABASE)\b",
"Удаление таблиц и баз — только вручную."),
]
for pattern, reason in BLOCKED:
if re.search(pattern, command, re.IGNORECASE):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason,
}
}, ensure_ascii=False))
breakПричина отказа возвращается агенту, и он может выбрать другой путь. Тот же эффект дают код выхода 2 с причиной в stderr или старый формат {"decision": "block", "reason": "..."}. А вернув "permissionDecision": "allow" вместе с updatedInput, хук может переписать команду — например, добавить к тестам флаг, который отключает интерактивный режим.
Если вам нужно только разрешать, спрашивать или запрещать команды по префиксу, проще взять rules. Хук нужен, когда решение требует логики: разобрать аргументы, посмотреть на ветку, сходить в файл.
Хук 2. Проверка перед концом хода
Stop срабатывает, когда агент считает работу законченной. Хук проверяет синтаксис изменённых PHP-файлов и, если есть ошибки, отправляет агента исправлять.
#!/usr/bin/env python3
"""Stop: не даёт закончить ход с синтаксическими ошибками в PHP."""
import json
import shutil
import subprocess
import sys
event = json.load(sys.stdin)
if event.get("stop_hook_active") or not shutil.which("php"):
print(json.dumps({"continue": True}))
sys.exit(0)
def git(*args, cwd):
return subprocess.run(["git", *args], capture_output=True, text=True,
cwd=cwd).stdout
root = git("rev-parse", "--show-toplevel", cwd=event["cwd"]).strip()
files = set(git("diff", "--name-only", "--diff-filter=ACMR", "HEAD", cwd=root).split())
files |= set(git("ls-files", "--others", "--exclude-standard", cwd=root).split())
errors = []
for path in sorted(f for f in files if f.endswith(".php")):
result = subprocess.run(["php", "-l", path], capture_output=True,
text=True, cwd=root)
if result.returncode != 0:
errors.append(result.stdout.strip() or result.stderr.strip())
if errors:
print(json.dumps({
"decision": "block",
"reason": "Исправь синтаксические ошибки PHP:\n" + "\n".join(errors),
}, ensure_ascii=False))
else:
print(json.dumps({"continue": True}))Для Stop ответ "decision": "block" не отклоняет ход, а продолжает его: Codex создаёт новое задание с текстом из reason. Поле stop_hook_active говорит, что ход уже продлевался этим хуком, — проверка в начале защищает от бесконечного цикла. Там же хук пропускает проверку, если php нет в PATH. И ещё: Stop ждёт на stdout именно JSON, обычный текст для этого события считается ошибкой.
Держите такие проверки быстрыми. Полный прогон тестов на каждый конец хода превратит работу в ожидание — для этого есть CI.
Хук 3. Контекст при старте
Самый простой хук — без скрипта, прямо в config.toml:
[[hooks.SessionStart]]
matcher = "startup|resume"
[[hooks.SessionStart.hooks]]
type = "command"
command = 'echo "Текущая ветка: $(git branch --show-current). Номер задачи — из имени ветки task-<номер>."'
timeout = 5Обычный текст из SessionStart попадает в контекст. Так можно подмешать ветку, номер задачи или напоминание о стенде без правки AGENTS.md.
Доверие
Хук — это код, который запускается от вашего имени. Поэтому неуправляемый хук не выполнится, пока вы не просмотрите и не подтвердите его. Откройте /hooks в CLI: там видны источники, новые и изменённые хуки, их можно подтвердить или выключить по одному. Доверие привязано к хэшу определения — любая правка хука снова требует проверки. Если хуки ждут проверки, Codex предупредит об этом при старте.
Отсюда рабочий порядок для команды: хуки лежат в .codex/ репозитория, меняются через MR с ревью, и каждый разработчик один раз подтверждает их у себя. Флаг --dangerously-bypass-hook-trust снимает проверку для одного запуска — он для автоматизации, которая проверяет хуки другим способом, а не для повседневной работы.
Администраторы могут задать управляемые хуки через requirements.toml; такие хуки считаются доверенными по политике, а с allow_managed_hooks_only = true Codex игнорирует все остальные.
Ограничения
- Хук — страховка, а не граница безопасности. Документация прямо говорит об этом: часть путей вызова инструментов может обходить хуки. Границу задают песочница и права.
- Сбой хука не блокирует действие. Если
PreToolUseвернул неподдерживаемое поле, Codex отметит сбой хука и выполнит вызов; ошибки и таймауты MCP-хуков тоже не блокируют операцию. Тестируйте хуки на реальных командах. - Совпавшие хуки запускаются параллельно. Один хук не может помешать запуску другого.
- Фоновые хуки ничего не блокируют. С
"async": trueхук не может запретить или переписать действие. - Вывод ограничен. Дополнительный контекст больше примерно 2500 токенов Codex сохраняет в файл и показывает модели сокращённо. Не выводите секреты: этот файл остаётся на диске.
Выключить хуки целиком — hooks = false в таблице [features].
Попробуйте сами
10–15 мин на рабочем месте- Положите
guard_bash.pyиhooks.jsonиз урока в.codex/своего репозитория, откройте/hooksи подтвердите хук. Попросите агента выполнитьgit push --forceи посмотрите, как он реагирует на отказ. - Адаптируйте
Stop-хук под свой стек:php -l,npx tsc --noEmitили быстрый линтер. Сделайте ошибку в файле и попросите агента закончить задачу. - Добавьте
SessionStart-хук с веткой и номером задачи в личный config.toml и проверьте, что агент знает номер задачи без подсказки.
Коротко
- Хуки — ваш код на событиях цикла:
PreToolUse,PostToolUse,Stop,SessionStartи другие. - Лежат в
hooks.jsonили[hooks]рядом с любым слоем конфигурации; проектные — только для доверенных проектов. PreToolUseзапрещает или переписывает вызов,Stopсdecision: blockпродолжает ход.- Каждый неуправляемый хук нужно подтвердить в
/hooks; изменение снова требует подтверждения. - Хуки — страховка, а не граница: сбой хука не блокирует действие, границу задают песочница и права.