Permission rules отвечают на вопрос, что агенту в принципе разрешено, но отвечают статически: правило смотрит на форму действия, а не на его содержание. Как только политика становится сложнее списка "это можно, это спрашивать", появляется потребность в проверке, которая читает конкретную команду, принимает решение по коду и оставляет след. Наивный ход - вписать эту логику словами в промпт или rule: "никогда не запускай деструктивные команды". Он кажется достаточным ровно до первого случая, когда важно, чтобы запрет сработал наверняка.
Ломается такой запрет на том, что инструкция в промпте - это просьба к модели, а не гарантия. Она зависит от того, как агент понял формулировку, и не оставляет доказуемого следа. Для настоящей политики нужен механизм, который выполняется детерминированно, не зависит от толкования и виден в логе. В Devin это hooks - проверяемая политика вокруг вызовов инструментов, живущая в файле .devin/hooks.v1.json, где объект hooks и есть всё содержимое файла, без обёртки.
Hook привязывается к событию жизненного цикла, и событий восемь. PreToolUse срабатывает до выполнения инструмента, PostToolUse - после; PermissionRequest - в момент, когда нужно решение о праве; UserPromptSubmit - когда пользователь отправил сообщение; Stop - когда агент собирается остановиться; PostCompaction - после успешного сжатия контекста; SessionStart и SessionEnd - на границах сессии. Разные события отвечают на разные вопросы: PreToolUse - "пропустить ли это действие", SessionStart - "что подготовить заранее", Stop - "можно ли считать работу законченной".
Внутри события правило устроено единообразно: поле matcher - это регулярное выражение, которое сверяется с именем инструмента (tool_name), а массив hooks перечисляет, что запустить при совпадении. Каждый элемент имеет тип - command для внешнего скрипта или prompt для инструкции модели - и необязательный timeout. Пример этой главы вешает на exec скрипт validate-agent-command.sh с лимитом в десять секунд: он получит управление до того, как команда выполнится.
Контракт command-hook стоит понимать точно, потому что именно он делает политику проверяемой. Скрипт получает данные события как JSON на stdin и возвращает решение через stdout и код выхода. Код 0 - успех, действие идёт дальше; код 2 - блокировка; любой другой код трактуется как ошибка, которая логируется, но не останавливает действие. В stdout можно вернуть поле decision со значением approve или block и пояснением reason, а через hookSpecificOutput - дописать контекст или переписать вход инструмента. Это и есть три возможности хука: разрешить, изменить или заблокировать, оставив след.
Почему политику выносят в скрипт, а не оставляют на усмотрение агента. Скрипт видит реальную команду, а не намерение; он один и тот же для разрешённого, запрещённого и испорченного ввода; его решение воспроизводимо и логируемо. Это переносит запрет из области "агент, пожалуйста, не делай" в область "система не даст сделать". Разница та же, что между надписью "не входить" и запертой дверью: первая полагается на добрую волю, вторая - на механизм.
За это платят дисциплиной написания хука. Он должен быть быстрым, потому что стоит на горячем пути перед каждым подходящим вызовом и ограничен timeout. Он должен быть детерминированным, потому что политика, дающая разные ответы на один вход, - это не политика. И он должен быть fail closed для запрещённого действия: если проверка не смогла отработать, безопасный исход - блокировка, а не пропуск. Хук, который при сбое молча пропускает команду, создаёт ложное чувство защиты - худшее из состояний.
Проверять хук нужно не на том, что он "написан", а на трёх входах явно. Разрешённая команда должна пройти и вернуть код 0. Запрещённая - быть заблокирована кодом 2 с внятным reason. Испорченный, неожиданный или пустой ввод не должен ни падать молча, ни случайно пропускать: правильная реакция - контролируемая блокировка. Пока эти три случая не прогнаны руками, о хуке известно только то, что он существует, но не то, что он работает.
Отдельно стоит не путать эти хуки с хуками Cascade. Cascade - агент эпохи Windsurf со своей моделью, и его хуки живут по другим путям, слушают другие события и подчиняются другому контракту. Механически перенести один файл в другой нельзя; при миграции старого проекта их переносит отдельный инструмент миграции, и делать это разумно с предварительным прогоном, а не вслепую. Пометка агента здесь так же важна, как и везде в Devin: hook "не сработал" часто означает, что он написан для другой среды.
Типичные провалы у хуков свои. Медленный или зависающий скрипт упирается в timeout и превращает защиту в тормоз на каждом действии. Хук, который логирует, но при ошибке пропускает, оставляет ощущение контроля без самого контроля. Политику пишут в промпт и считают её выполненной, хотя доказуемого следа нет. И, наконец, событие выбирают не то - вешают проверку на PostToolUse, когда помешать нужно было в PreToolUse, до выполнения. Признак всех этих ошибок один: хук считают готовым, ни разу не проверив на запрещённом и на испорченном входе. Прогнать оба - и большинство сюрпризов не случится.
{
"PreToolUse": [
{
"matcher": "exec",
"hooks": [
{
"type": "command",
"command": "./scripts/validate-agent-command.sh",
"timeout": 10
}
]
}
]
}