Перейти к содержимому

Машинный перевод. Авторитетным источником является английская версия; проверка носителем языка ещё не выполнена.

Claude Code

Внутри хуков PEP: политика deny-closed внутри Claude Code

Автор Olivares AI 8 мин чтения

Команда платформы обеспечивает Claude Code по всей своей инженерной организации. Чтобы обеспечить соблюдение политики звонков инструментов, они подключают сервер хуков — локальный процесс, который получает события хуков через stdin и возвращает решение в формате JSON через stdout. Первая неделя проходит нормально. PreToolUse срабатывает перед каждым вызовом инструмента, хук возвращает deny для всего, что касается продакшена, и команда считает, что принудительное исполнение работает.

Затем Claude Code выпускает новую версию. PermissionRequest начинает срабатывать вместе с PreToolUse. Сервер хуков возвращает тот же JSON permissionDecision, который он уже возвращал. Claude Code принимает ответ, парсит его, не находит ни одного поля, которое бы он распознавал для этого события, и продолжает работу так, как будто решение не было предоставлено. Отказ безмолвно игнорируется. Точка принудительного исполнения команды теперь представляет собой стену с gap в ней, и ни в одном из логов это не отражается.

Вот проблема, которую должен решить управляемый PEP: жизненный цикл хуков Claude Code не представляет собой одно событие с одним форматом передачи данных. Это примерно 30 событий, каждое с другой схемой вывода, которой придерживается время выполнения, и ошибка в схеме неотличима от отсутствия сигнала.

Механизм передачи данных является контрактом безопасности

Причина, по которой одно поле permissionDecision не работает везде, заключается в том, что события хуков Claude Code развивались независимо. Форма вывода, которая управляет вызовом инструмента, не та форма, которая управляет отправкой запроса, и не та форма, которая останавливает задачу.

Существует шесть различных механизмов передачи данных:

МеханизмФорма выводаСобытия
permissionDecisionhookSpecificOutput.permissionDecision (разрешить /deny/ask/defer)Предварительное использование инструмента
permissionBehaviorhookSpecificOutput.decision.behavior (разрешить /deny)Запрос разрешения
topLevelDecisionВерхний уровень decision: "block" + reasonОтправка запроса пользователя, Расширение запроса пользователя, Предварительная компактизация, Изменение конфигурации, Пост-набор инструментов
continueFalsecontinue: false + stopReasonЗадача создана, Задача выполнена, Сотрудник бездействует
postToolUsedecision: "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 хук PEP: поток классификации событий и проводного механизма

Дефолт 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, получает несколько свойств, которые имеют значение:

  1. Никакого скрытого разрешения. Каждое событие hook классифицируется, и каждое неопознанное событие по умолчанию отклоняется. Новый выпуск Claude Code не может вводить неконтролируемое событие жизненного цикла.
  2. Правильная форма данных для каждого события. PEP не возвращает общий JSON-объект и не надеется, что Claude Code его уважит. Он возвращает точную схему вывода, ожидаемую конкретным событием, потому что отклонение в неправильной схеме не является отклонением.
  3. Честное невыполнение принуждения. События, которые не могут быть принудительно выполнены (наблюдаемые события, события с инвертированным управлением), не притворяются выполненными. PEP регистрирует их, возвращает нейтральный результат и не даёт оператору ложного чувства контроля.
  4. Защита от вмешательства при распространении. Слой управляемых настроек обеспечивает невозможность переопределения хука PEP и помечает конфигурации, где его можно обойти.

PEP является исполнительной частью более широкой управляемой модели работы. Карта доступа, журнал аудита и слой политики в виде кода вокруг него освещены в обзоре продукта и в документации по хукам. Если вы хотите увидеть, как составляются путь телеметрии и контроль разрешений, обзор архитектуры пошагово рассматривает оба.

Похожие материалы

Часто задаваемые вопросы

Почему PEP по умолчанию устанавливает неизвестные события хука в deny вместо allow?

Молчаливое разрешение на нераспознанное событие означает, что любой новый хук Claude Code, поставляемый в будущей версии, обойдет меры принудительного соблюдения до тех пор, пока кто-то вручную не добавит его в карту классификации. Это противоположно позиции безопасности, необходимой для управляемого развертывания. По умолчанию «deny-closed» рассматривает неизвестное событие как ворота разрешений: оно наблюдается, никогда не отбрасывается, и если ни одно правило политики не соответствует ему, оно отклоняется. Это гарантирует, что новые события жизненного цикла управляются с момента их появления, а не с момента, когда кто-то заметит, что они не были добавлены.

Что произойдет, если PEP выдаст неправильную форму данных для ответа хуком?

Claude Code тихо игнорирует это. Каждое событие hook соответствует определённой схеме вывода: PreToolUse ожидает hookSpecificOutput.permissionDecision, PermissionRequest ожидает hookSpecificOutput.decision.behavior, а другие события блокировки ожидают верхнеуровневое поле decision или continue. Если PEP возвращает действительный объект JSON, но в неправильной схеме для этого события, Claude Code считает это отсутствием решения и продолжает выполнение. Именно поэтому механизм wire является частью контракта безопасности, а не декоративной деталью: отказ, который Claude Code игнорирует, не является отказом.

Узнайте, до чего могут добраться ваши агенты

Olivares AI — открытая self-hosted платформа для управления вашим парком AI. Разверните её на собственной инфраструктуре и получите карту доступа, которую давно запрашивают ваши команды безопасности и платформ.