Блог • AI / Agent

Как реализовать права Claude Agent? От Permission Policy и MCP Tool Call к JSON-очереди согласования

Контроль прав Claude Agent — это JSON-решение между вызовом инструмента, который предложила модель, и кодом, который реально выполняется. Политика даёт allow, deny или ask. Ask становится записью согласования. Решение возвращается на тот же tool_use_id. Если id потерян или неверен, сессия останавливается или тот же вызов выполняется дважды.

Как платформа решает сама, написано в предыдущей статье. Там auto значит, что сервер читает permission_policy и пишет evaluated_permission. Этот разбор вы не меняете. Здесь — три JSON, которые подключаете вы: политика, очередь согласования и возврат отказа. Собственные инструменты и вызовы, видимые только в колбэке, идут сюда.

В этой статье:

Сначала запомните: если инструмент записан как allow, очередь его не видит. canUseTool не срабатывает для уже разрешённого вызова. Дальше: точка подключения, JSON политики, очередь, возврат.

Куда вы подключаетесь в официальной проверке

Agent SDK и Claude Code проверяют правила в фиксированном порядке. См. права Agent SDK и права Claude Code. Сначала deny, затем ask, затем allow. Более конкретный allow не отменяет уже совпавший deny.

  1. 1
    deny останавливает вызов

    Совпавший deny блокирует вызов, в том числе в режиме bypassPermissions. Голое имя инструмента, например Bash, убирается из контекста ещё до этого шага.

  2. 2
    ask попадает в колбэк

    Совпавший ask передаётся в canUseTool, даже если включён режим обхода. В dontAsk запроса нет: совпавший вызов отклоняется, колбэк не вызывается.

  3. 3
    allow больше не спрашивает

    Совпавший allow запускает инструмент, и canUseTool не вызывается. Одно имя инструмента в списке allow действует так же, если отдельное правило ask не возвращает вызов в колбэк.

  4. 4
    Ничего не совпало

    Если раньше ничего не совпало, только режим default отдаёт вызов в canUseTool. dontAsk отклоняет его и не вызывает колбэк.

Поэтому очередь получает только ещё не разрешённые вызовы. Чтобы учитывать каждый вызов, используйте хук PreToolUse. Он выполняется до остальных проверок и может разрешить, запретить или изменить аргументы, но не отменяет уже совпавшие deny или ask.

Три точки подключения делят одну запись очереди. Меняется только официальная форма возврата.

Подключение JSON, который вы храните Форма ответа модели
Размещённый инструмент Managed Agents, остановлен на ask permission_policy user.tool_confirmation
Колбэк Agent SDK Ваша policy и запись очереди { behavior, updatedInput / message }
Свой инструмент Ваша policy и запись очереди user.custom_tool_result

Свой инструмент приходит как agent.custom_tool_use и не имеет evaluated_permission. Документация: политики прав Managed Agents. Если auto запретил размещённый инструмент, клиент не может отменить это событием подтверждения. Не делайте очередь вторым каналом принудительного allow.

Как писать Permission Policy JSON

Если вы запускаете только Claude Code, строк allow / ask / deny в официальном settings.json достаточно. Агенту внутри продукта нужен документ, который можно версионировать, сравнивать и проверять схемой. Документ ниже — контракт вашего приложения, а не тело запроса Anthropic.

Политика приложения: default равен ask, deny важнее более позднего allow
{
  "version": 1,
  "default": "ask",
  "rules": [
    {
      "id": "deny-env",
      "effect": "deny",
      "tool": "Read",
      "input": { "file_path": { "const": ".env" } },
      "reason_code": "secret_file"
    },
    {
      "id": "ask-pr-main",
      "effect": "ask",
      "tool": "mcp__github__create_pull_request",
      "input": { "base": { "enum": ["main", "master"] } },
      "reason_code": "protected_branch"
    },
    {
      "id": "allow-docs-search",
      "effect": "allow",
      "tool": "mcp__docs__search"
    }
  ]
}
Поле Смысл
version Версия формата политики. Скопируйте её в запись очереди, чтобы старую заявку не читать новыми правилами.
default Эффект, если ни одно правило не совпало. В продукте используйте ask или deny, не allow.
effect deny, ask или allow
tool Полное имя инструмента. Инструмент MCP начинается с mcp__, затем имя сервера, затем имя инструмента.
input Необязательно. Если указано, аргументы тоже должны совпасть. Если опустить, сравнивается только имя инструмента.
reason_code Стабильный машинный код. Пишите его в возврат и в аудит. Не используйте фразу как первичный ключ.

Порядок как в официальной проверке: все deny, затем ask, затем allow. Внутри одного effect побеждает более раннее правило в файле.

Когда default равен ask, новый инструмент MCP без правила попадает в очередь, а не выполняется. Это та же идея, что у размещённого MCP toolset со значением по умолчанию always_ask, только документ и очередь ваши.

Как один вызов MCP совпадает с правилом

Вызов модели несёт как минимум тип, id, имя инструмента и input. Имена полей следуют блоку tool_use Messages API. Имя инструмента MCP соединяет имя сервера и имя инструмента.

Вызов, который открывает pull request в защищённую ветку
{
  "type": "tool_use",
  "id": "toolu_01AbC",
  "name": "mcp__github__create_pull_request",
  "input": {
    "base": "main",
    "title": "Retry failed checks",
    "head": "fix/retry"
  }
}
  1. 1
    Сначала проверьте форму

    Проверьте input собственной JSON Schema этого инструмента. При ошибке: не ставить в очередь, не вызывать MCP, сразу deny, reason_code schema_invalid.

  2. 2
    Затем сопоставьте deny, ask, allow

    Вызов выше попадает в ask-pr-main, потому что base есть в enum. Порядок в файле важен только внутри одного effect.

  3. 3
    Не разрешайте только по имени инструмента

    Правило allow без ограничения input пропустит и base, равный main. Ограничения аргументов — часть прав, а не копия схемы.

Сопоставление только равенства и enum. Не склеивайте строки правил в shell.
function decide(policy, call) {
  const order = ["deny", "ask", "allow"];
  for (const effect of order) {
    const hit = policy.rules.find(
      (rule) => rule.effect === effect && matchRule(rule, call)
    );
    if (hit) {
      return {
        effect,
        rule_id: hit.id,
        reason_code: hit.reason_code || null
      };
    }
  }
  return { effect: policy.default, rule_id: null, reason_code: "default" };
}

function matchRule(rule, call) {
  if (rule.tool !== call.name) return false;
  if (!rule.input) return true;
  return Object.entries(rule.input).every(([key, cond]) => {
    const value = call.input?.[key];
    if (Object.prototype.hasOwnProperty.call(cond, "const")) return value === cond.const;
    if (Array.isArray(cond.enum)) return cond.enum.includes(value);
    return false;
  });
}

Префиксы путей проверяет отдельный код по нормализованному пути. Если функция выше видит непонятное условие, она возвращает false: правило не совпало и не разрешило вызов.

JSON Schema Permission Policy
Вопрос Форма аргументов верна? Можно ли выполнить этот вызов?
При ошибке schema_invalid, и не ставить в очередь deny / ask
Пример base должен быть строкой base равен main

Что хранит очередь согласования

Когда effect равен ask, сначала сохраните запись, потом ждите. Не держите ожидание только в памяти. Если процесс перезапустится, у вызова модели не будет результата.

Одна запись на согласовании. tool_use_id уникален.
{
  "id": "apr_01HZXK",
  "session_id": "sess_01",
  "tool_use_id": "toolu_01AbC",
  "tool_name": "mcp__github__create_pull_request",
  "input": {
    "base": "main",
    "title": "Retry failed checks",
    "head": "fix/retry"
  },
  "matched_rule": "ask-pr-main",
  "policy_version": 1,
  "status": "pending",
  "created_at": "2026-09-23T09:00:00Z",
  "expires_at": "2026-09-23T09:15:00Z"
}

Решение человека храните отдельным документом. Не перезаписывайте им запись очереди.

Решение о согласовании. actor берётся из сессии входа.
{
  "approval_id": "apr_01HZXK",
  "tool_use_id": "toolu_01AbC",
  "decision": "deny",
  "actor": "user:alex",
  "reason_code": "protected_branch",
  "decided_at": "2026-09-23T09:04:12Z"
}

actor берётся из вашей сессии входа. Не доверяйте полю пользователя внутри input.

Как возвращать deny и allow

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

Ваше решение Колбэк Agent SDK Размещённый инструмент, остановленный на ask Свой инструмент
allow { behavior: "allow", updatedInput } result: "allow" После успешного выполнения верните результат своего инструмента
deny { behavior: "deny", message } result: "deny" + deny_message Результат своего инструмента с признаком ошибки, включая reason_code

См. официальную документацию по вводу пользователя. Allow в TypeScript включает updatedInput, в Python — updated_input. До Claude Code v2.1.207 пропуск этого поля не проходил проверку и давал отказ. Всегда возвращайте исходный input.

Deny, который видит модель. Укажите id правила и reason_code и напишите не повторять те же аргументы.
{
  "behavior": "deny",
  "message": "Denied by rule ask-pr-main (protected_branch). Do not retry this call."
}

message и deny_message видит модель. Укажите id правила и reason_code. Не пишите внутренние имена хостов, токены и личные заметки проверяющего.

Строку аудита храните отдельно от возврата. Ставьте wrote_back в true только после успешного возврата.
{
  "tool_use_id": "toolu_01AbC",
  "effect": "deny",
  "rule_id": "ask-pr-main",
  "reason_code": "protected_branch",
  "wrote_back": true
}

Вызывайте MCP только после allow. deny и expired его не вызывают. Движок политики не выполняет инструмент сначала, чтобы спросить человека потом.

Чем это отличается от settings и auto

Три набора правил отвечают на разные вопросы. Не держите три списка в надежде, что они совпадут сами.

Настройки Claude Code Managed Agents Ваша policy и очередь
Форма правила Строка из инструмента и уточнения always_allow / always_ask / auto effect плюс имя инструмента и необязательное ограничение input
Кто оценивает Claude Code, порядок deny, ask, allow Платформа. auto оценивает этот один вызов Ваш код, в том же порядке, что официальный поток
Где решает человек Интерактивный запрос или canUseTool user.tool_confirmation Ваша очередь, затем отображение на два возврата слева
Подходит для Локальной разработки Размещённого агента Продукта, которому нужны заявки и аудит
Правила прав Claude Code. Это официальная форма settings, а не политика приложения выше.
{
  "permissions": {
    "allow": ["Bash(npm test *)"],
    "ask": ["Bash(git push *)"],
    "deny": ["Read(./.env)", "Read(./.env.*)"]
  }
}

Инструменты, которые человек обязан увидеть, остаются с effect ask. Если auto считает вызов безопасным, он выполняется до того, как его кто-то увидит. Подробности в предыдущей статье. В продукте источник — ваша policy. Экспорт строки deny для Claude Code — только проекция. Состояние согласования остаётся в очереди.

Проверить три JSON в JSONNote

Перед выпуском прогоните политику, запись очереди и возврат через рабочую область. Все три можно отформатировать, проверить и сравнить в браузере, без живого агента.

  1. 1
    Отформатировать политику

    Откройте политику в форматировании JSON. Настройки Claude Code тоже строгий JSON. Комментарий или висячая запятая дадут ошибку при следующем запуске.

  2. 2
    Проверить запись очереди

    Вставьте схему ниже в инструмент JSON Schema, затем одну запись согласования. Нет tool_use_id или status вне перечисления — проверка не пройдёт.

  3. 3
    Сравнить изменённые правила

    До и после изменения правила используйте JSON Diff, чтобы увидеть, не стало ли ask значением allow. Это важнее формулировки.

  4. 4
    Проверить возврат

    Через проверку JSON убедитесь, что возврат всё ещё корректный JSON и tool_use_id на месте.

JSON Schema записи согласования. Её можно вставить в инструмент Schema как есть.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "session_id",
    "tool_use_id",
    "tool_name",
    "input",
    "status",
    "created_at",
    "expires_at"
  ],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "session_id": { "type": "string", "minLength": 1 },
    "tool_use_id": { "type": "string", "minLength": 1 },
    "tool_name": { "type": "string", "minLength": 1 },
    "input": { "type": "object" },
    "matched_rule": { "type": ["string", "null"] },
    "policy_version": { "type": "integer", "minimum": 1 },
    "status": { "enum": ["pending", "approved", "denied", "expired"] },
    "created_at": { "type": "string", "format": "date-time" },
    "expires_at": { "type": "string", "format": "date-time" }
  }
}

Вопросы

Почему canUseTool не вызывается?

Более раннее правило allow или режим вроде acceptEdits или bypassPermissions уже разрешили вызов. Колбэк видит только ещё не одобренные вызовы. Чтобы смотреть каждый вызов, используйте PreToolUse. Deny он всё равно не отменяет.

Достаточно ли проверки JSON Schema?

Нет. Схема доказывает только форму, например что base — строка. Нужен ли человек, когда base равен main, решает policy. Верная форма всё равно может быть deny или ask.

После отказа модели стоит повторять вызов с теми же аргументами?

Нет. В ответе напишите не повторять те же аргументы и не удаляйте результат, будто вызова не было. Новая попытка требует новый tool_use_id и новый проход через очередь.

Просроченная запись по умолчанию разрешается?

Нет. Пометьте запись expired и верните deny с reason_code approval_expired. Истечение срока — не согласие.

Нужна ли эта очередь, если Managed Agents использует auto?

Да, но только для always_ask и собственных инструментов. Вызов, который auto пометил как deny, событие подтверждения не отменяет. Не делайте из очереди обход, который принудительно ставит allow.

Итог

Реализовать контроль прав — значит вести три JSON: политику, которую вы пишете, запись очереди для ask и решение, которое возвращается в конечном статусе. Порядок как в официальной проверке, deny первый. Схема смотрит только форму. Если человек ещё не решил или заявка просрочена, MCP не вызывайте.

Прямой вывод: по умолчанию ask или deny. Allow обязан назвать инструмент и, когда это важно, аргументы. Каждый tool_use_id выполняется не больше одного раза и всегда получает возврат.

Кто объявляет инструмент MCP и кто его выполняет, разобрано в безопасности MCP. Почему JSON стоит перед выполнением, разобрано в границе безопасности.

Дальше: проверьте форму записи согласования, затем отдайте политику агенту.

Назад в блог