Команда платформы обеспечивает Claude Code по всей своей инженерной организации. Чтобы обеспечить соблюдение политики звонков инструментов, они подключают сервер хуков — локальный процесс, который получает события хуков через stdin и возвращает решение в формате JSON через stdout. Первая неделя проходит нормально. PreToolUse срабатывает перед каждым вызовом инструмента, хук возвращает deny для всего, что касается продакшена, и команда считает, что принудительное исполнение работает.
Затем Claude Code выпускает новую версию. PermissionRequest начинает срабатывать вместе с PreToolUse. Сервер хуков возвращает тот же JSON permissionDecision, который он уже возвращал. Claude Code принимает ответ, парсит его, не находит ни одного поля, которое бы он распознавал для этого события, и продолжает работу так, как будто решение не было предоставлено. Отказ безмолвно игнорируется. Точка принудительного исполнения команды теперь представляет собой стену с gap в ней, и ни в одном из логов это не отражается.
Вот проблема, которую должен решить управляемый PEP: жизненный цикл хуков Claude Code не представляет собой одно событие с одним форматом передачи данных. Это примерно 30 событий, каждое с другой схемой вывода, которой придерживается время выполнения, и ошибка в схеме неотличима от отсутствия сигнала.
Механизм передачи данных является контрактом безопасности
Причина, по которой одно поле permissionDecision не работает везде, заключается в том, что события хуков Claude Code развивались независимо. Форма вывода, которая управляет вызовом инструмента, не та форма, которая управляет отправкой запроса, и не та форма, которая останавливает задачу.
Существует шесть различных механизмов передачи данных:
| Механизм | Форма вывода | События |
|---|---|---|
permissionDecision | hookSpecificOutput.permissionDecision (разрешить /deny/ask/defer) | Предварительное использование инструмента |
permissionBehavior | hookSpecificOutput.decision.behavior (разрешить /deny) | Запрос разрешения |
topLevelDecision | Верхний уровень decision: "block" + reason | Отправка запроса пользователя, Расширение запроса пользователя, Предварительная компактизация, Изменение конфигурации, Пост-набор инструментов |
continueFalse | continue: false + stopReason | Задача создана, Задача выполнена, Сотрудник бездействует |
postToolUse | decision: "block" (блокирует дальнейшую обработку; инструмент уже использован) | После использования инструмента |
neutral | Нет обязательного блока | Все события context/observe, плюс инвертированные события, такие как Stop |
Если PEP возвращает permissionDecision: "deny" на событие PermissionRequest, Claude Code игнорирует это — это событие ожидает decision.behavior, а не permissionDecision. JSON корректен, HTTP-ответ 200, и отказ не существует. Это не теоретический пограничный случай; это документированное соглашение по проводам, проверенное по code.claude.com/docs/en/hooks.
Трехступенчатая классификация: gating, context, observe
Не каждое событие hook может управлять действием. Попытка отклонить SessionEnd или Notification бессмысленна — эти события информационные и не несут контроль над решением. Попытка отклонить событие Stop хуже чем бессмысленна: decision: "block" на Stop поддерживает работу агента, что противоположно безопасной остановке.
PEP классифицирует каждое признанное событие hook в одну из трех категорий:
События Gating несут решение, которое может разрешить, отказать или заблокировать действие. Они являются поверхностью принуждения. Но не все события Gating одинаково применимы: Stop и SubagentStop классифицируются как gating в собственной таксономии Claude Code, но их семантика блокировки инвертирована (блокировка = продолжать выполнение), поэтому PEP рассматривает их как нейтральные — он никогда не посылает блокировку, которая удерживала бы агента в живых против намерения оператора. Аналогично, Elicitation и ElicitationResult имеют классификацию gating, но в текущей версии отсутствует механизм выполняемого действия, поэтому PEP по умолчанию относит их к нейтральным, а не притворяется, что принуждение существует там, где его нет.
Контекст события позволяют PEP внедрять additionalContext или переписывать вывод, но не могут действительно блокировать действие. PostToolUse является исключением partial — оно может блокировать дальнейшую обработку помеченного вывода, но инструмент уже выполнился. Остальные (PermissionDenied, MessageDisplay, SessionStart, Setup, SubagentStart, PostCompact, InstructionsLoaded, PostToolUseFailure) — это наблюдение с контекстом: полезны для обогащения представления модели, а не для её остановки.
Наблюдение события (Notification, SessionEnd, StopFailure, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove) вообще не имеют управленческой функции. PEP регистрирует их для телеметрии и инвентаризации SIEM, отвечает нейтрально и продолжает работу.
Эта классификация не является рекомендательной. Она определяет, применяет ли решатель composition-root правило политики (gating + enforceable), внедряет контекст (context) или просто наблюдает (observe). Ошибка в этом означает либо ложное выполнение (возврат отказа, который игнорируется), либо пропущенное выполнение (обращение с событием gating как с observe).
Карта hookSpecs: единый источник правды
Классификация, механизм передачи сигнала (wire) и флаг применимости (enforceability) находятся в одной карте. Это фактический код, который использует коннектор — как HTTP-рендерер ответа, так и решатель composition-root читают из него:
type hookSpec struct {
class string // "gating" | "context" | "observe"
mech hookMech
enforceable bool
}
var hookSpecs = map[string]hookSpec{
// GATING — возврат хука может применить allow/deny/block к действию.
"PreToolUse": {"gating", mechPermissionDecision, true},
"PermissionRequest": {"gating", mechPermissionBehavior, true},
"UserPromptSubmit": {"gating", mechTopLevelDecision, true},
"UserPromptExpansion": {"gating", mechTopLevelDecision, true},
"PreCompact": {"gating", mechTopLevelDecision, true},
"ConfigChange": {"gating", mechTopLevelDecision, true},
"PostToolBatch": {"gating", mechTopLevelDecision, true},
"TaskCreated": {"gating", mechContinueFalse, true},
"TaskCompleted": {"gating", mechContinueFalse, true},
"TeammateIdle": {"gating", mechContinueFalse, true},
"Stop": {"gating", mechNeutral, false}, // инвертировано
"SubagentStop": {"gating", mechNeutral, false}, // инвертировано
"Elicitation": {"gating", mechNeutral, false}, // не подключено в v1
"ElicitationResult": {"gating", mechNeutral, false},
// CONTEXT — additionalContext / изменение вывода, без реальной блокировки.
"PostToolUse": {"context", mechPostToolUse, true},
"PostToolUseFailure": {"context", mechNeutral, false},
"PermissionDenied": {"context", mechNeutral, false},
"MessageDisplay": {"context", mechNeutral, false},
"SessionStart": {"context", mechNeutral, false},
"Setup": {"context", mechNeutral, false},
"SubagentStart": {"context", mechNeutral, false},
"PostCompact": {"context", mechNeutral, false},
"InstructionsLoaded": {"context", mechNeutral, false},
// OBSERVE — без контроля решений.
"Notification": {"observe", mechNeutral, false},
"SessionEnd": {"observe", mechNeutral, false},
"StopFailure": {"observe", mechNeutral, false},
"CwdChanged": {"observe", mechNeutral, false},
"FileChanged": {"observe", mechNeutral, false},
"WorktreeCreate": {"observe", mechNeutral, false},
"WorktreeRemove": {"observe", mechNeutral, false},
}
Тридцать событий, каждое с точно одной классификацией, одним проводным механизмом и одним вердиктом о принудительном исполнении. Рендерер обращается к hookMechFor(event), чтобы определить, какую форму JSON выдавать. Решающий элемент обращается к HookEnforcementFor(event), чтобы определить, применять ли правила политики, внедрять контекст или наблюдать. Оба читают из одной и той же карты, поэтому они не могут расходиться во мнениях.
Дефолт deny-closed
Самая важная функция в файле состоит из четырех строк:
func hookSpecFor(event string) hookSpec {
if s, ok := hookSpecs[event]; ok {
return s
}
return hookSpec{class: "unknown", mech: mechPermissionDecision, enforceable: true}
}
Событие, которого нет в карте — потому что Claude Code отправил новое событие hook, которое коннектор еще не классифицировал — обрабатывается как unknown, присваивается ему проводной механизм mechPermissionDecision и помечается как обязательное к исполнению. Это настройка по умолчанию «deny-closed»: неклассифицированное событие является воротами разрешений, а не тихим пропуском. Если ни одно правило политики не совпадает, применяется конфигурированная оператором настройка по умолчанию, которая в управляемом развертывании означает отказ (deny).
Альтернатива — установка по умолчанию в нейтральное положение или наблюдение — означала бы, что каждое новое событие hook, которое вводит Claude Code, остается неконтролируемым до того момента, пока кто-то не заметит его и не добавит в карту. В модели «deny-closed» новое событие подчиняется правилам с момента его срабатывания, даже если классификация консервативна. Ложный отказ на новом событии видим и исправим; тихое разрешение невидимо и может сохраняться месяцами.
Структура HookEnforcement экспортирует эту классификацию, чтобы решающий компонент мог различать три уровня событий управления доступом:
- Классический шлюз (
PreToolUse,PermissionRequest,PostToolUseи любое неизвестное событие): когда ни одно управляемое правило не совпадает, применяется политика по умолчанию оператора (deny-closed). - Шлюз жизненного цикла (другие применимые события управления, такие как
UserPromptSubmit,TaskCreated): решающий компонент применяет безопасный по умолчанию подход для каждого события — нейтрально для событий UX/lifecycle, запрет для событий изменения состояния. - Неисполняемый шлюз (
Stop,SubagentStop,Elicitation): решающий компонент всегда возвращает нейтральный результат. Генерация запрета, который Claude Code интерпретирует как «продолжать выполнение», была бы противоположна безопасному поведению.
Распространение: от классификации к флоту
Правильная классификация событий — это лишь половина дела. Другая половина — установка PEP на каждый экземпляр Claude Code. Коннектор управляемых настроек преобразует конфигурацию хуков в форму, ожидаемую Claude Code, и распространяет её как файл настроек, управляемый сервером — конфигурацию, которую нельзя переопределить и которую управляющая плоскость отправляет на каждый управляемый хост.
Хук PEP распространяется с пустым сопоставителем (соответствует всем инструментам — ни один инструмент не избегает точки принуждения) и связывается с allowManagedHooksOnly, чтобы предотвратить подрывание управляемого PEP локальными хуками разработчика. Без этого флага хук уровня пользователя мог бы затмить управляемый хук, и принуждение выглядело бы активным, хотя на самом деле его можно было бы обойти. Проверка во время создания ловит это: если политика рассылает PreToolUse PEP-хук без allowManagedHooksOnly, консоль выдаёт уведомление о возможной подделке.
Путь телеметрии отдельный и безплановый: экспорт OpenTelemetry для Claude Code включен через управляемые переменные окружения (CLAUDE_CODE_ENABLE_TELEMETRY, ключи экспортера OTEL_*), чтобы управляющая плоскость могла наблюдать использование подписки без проксирования инференса или доступа к учётным данным подписки. Захват контента (OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_CONTENT) по умолчанию отключен; включение его является сознательным, отмеченным выбором, поскольку он отправляет содержимое подсказок и инструментов с машины разработчика, создавая обязанность по хранению и редактированию, которую должна нести управляющая плоскость.
Что это означает для управляемого развертывания
Команда, использующая Claude Code с управляемым PEP, получает несколько свойств, которые имеют значение:
- Никакого скрытого разрешения. Каждое событие hook классифицируется, и каждое неопознанное событие по умолчанию отклоняется. Новый выпуск Claude Code не может вводить неконтролируемое событие жизненного цикла.
- Правильная форма данных для каждого события. PEP не возвращает общий JSON-объект и не надеется, что Claude Code его уважит. Он возвращает точную схему вывода, ожидаемую конкретным событием, потому что отклонение в неправильной схеме не является отклонением.
- Честное невыполнение принуждения. События, которые не могут быть принудительно выполнены (наблюдаемые события, события с инвертированным управлением), не притворяются выполненными. PEP регистрирует их, возвращает нейтральный результат и не даёт оператору ложного чувства контроля.
- Защита от вмешательства при распространении. Слой управляемых настроек обеспечивает невозможность переопределения хука PEP и помечает конфигурации, где его можно обойти.
PEP является исполнительной частью более широкой управляемой модели работы. Карта доступа, журнал аудита и слой политики в виде кода вокруг него освещены в обзоре продукта и в документации по хукам. Если вы хотите увидеть, как составляются путь телеметрии и контроль разрешений, обзор архитектуры пошагово рассматривает оба.