Amiga
Оглавление курса · урок 13 из 16

Модуль 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 матчера нет.

json · .codex/hooks.json
{
  "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.

python · .codex/hooks/guard_bash.py
#!/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-файлов и, если есть ошибки, отправляет агента исправлять.

python · .codex/hooks/php_lint_on_stop.py
#!/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:

toml · ~/.codex/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 мин на рабочем месте
  1. Положите guard_bash.py и hooks.json из урока в .codex/ своего репозитория, откройте /hooks и подтвердите хук. Попросите агента выполнить git push --force и посмотрите, как он реагирует на отказ.
  2. Адаптируйте Stop-хук под свой стек: php -l, npx tsc --noEmit или быстрый линтер. Сделайте ошибку в файле и попросите агента закончить задачу.
  3. Добавьте SessionStart-хук с веткой и номером задачи в личный config.toml и проверьте, что агент знает номер задачи без подсказки.

Коротко

  • Хуки — ваш код на событиях цикла: PreToolUse, PostToolUse, Stop, SessionStart и другие.
  • Лежат в hooks.json или [hooks] рядом с любым слоем конфигурации; проектные — только для доверенных проектов.
  • PreToolUse запрещает или переписывает вызов, Stop с decision: block продолжает ход.
  • Каждый неуправляемый хук нужно подтвердить в /hooks; изменение снова требует подтверждения.
  • Хуки — страховка, а не граница: сбой хука не блокирует действие, границу задают песочница и права.
Отметка хранится только в вашем браузере
Хотите связаться с владельцем
компании напрямую?
Дмитрий Тарасов
Дмитрий Тарасов
СЕО

НАПИСАТЬ

Написать в MAX