Блог • 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, которые подключаете вы: политика, очередь согласования и возврат отказа. Собственные инструменты и вызовы, видимые только в колбэке, идут сюда.
В этой статье:
- На каком шаге официальной проверки вызывается
canUseTool - Поля, значения по умолчанию и порядок сопоставления Permission Policy JSON
- Как вызов MCP сначала проходит схему, а потом совпадает с правилом
- Какие поля хранит запись очереди и что делает истечение срока
- Как возвращать allow и deny, чтобы один вызов не выполнился дважды
Сначала запомните: если инструмент записан как allow, очередь его не видит. canUseTool не срабатывает для уже разрешённого вызова. Дальше: точка подключения, JSON политики, очередь, возврат.
Куда вы подключаетесь в официальной проверке
Agent SDK и Claude Code проверяют правила в фиксированном порядке. См. права Agent SDK и права Claude Code. Сначала deny, затем ask, затем allow. Более конкретный allow не отменяет уже совпавший deny.
-
1
deny останавливает вызов
Совпавший deny блокирует вызов, в том числе в режиме
bypassPermissions. Голое имя инструмента, например Bash, убирается из контекста ещё до этого шага. -
2
ask попадает в колбэк
Совпавший ask передаётся в
canUseTool, даже если включён режим обхода. ВdontAskзапроса нет: совпавший вызов отклоняется, колбэк не вызывается. -
3
allow больше не спрашивает
Совпавший allow запускает инструмент, и
canUseToolне вызывается. Одно имя инструмента в списке allow действует так же, если отдельное правило ask не возвращает вызов в колбэк. -
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.
{
"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 соединяет имя сервера и имя инструмента.
{
"type": "tool_use",
"id": "toolu_01AbC",
"name": "mcp__github__create_pull_request",
"input": {
"base": "main",
"title": "Retry failed checks",
"head": "fix/retry"
}
}
-
1
Сначала проверьте форму
Проверьте input собственной JSON Schema этого инструмента. При ошибке: не ставить в очередь, не вызывать MCP, сразу deny, reason_code
schema_invalid. -
2
Затем сопоставьте deny, ask, allow
Вызов выше попадает в
ask-pr-main, потому чтоbaseесть в enum. Порядок в файле важен только внутри одного effect. -
3
Не разрешайте только по имени инструмента
Правило allow без ограничения input пропустит и base, равный main. Ограничения аргументов — часть прав, а не копия схемы.
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, сначала сохраните запись, потом ждите. Не держите ожидание только в памяти. Если процесс перезапустится, у вызова модели не будет результата.
{
"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"
}
tool_use_idуникален. Если тот же id приходит снова, верните существующую запись и не вызывайте MCP второй раз.- status может перейти только из
pendingвapproved,deniedилиexpired. Конечный статус не меняется. - После
expires_atпометьте еёexpiredи вернитеdenyс reason_codeapproval_expired. Не разрешайте по умолчанию. - Храните input как его прислали, чтобы проверяющий мог его прочитать. Не читайте политику из input. Модель не может менять правила через аргументы.
Решение человека храните отдельным документом. Не перезаписывайте им запись очереди.
{
"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.
{
"behavior": "deny",
"message": "Denied by rule ask-pr-main (protected_branch). Do not retry this call."
}
message и deny_message видит модель. Укажите id правила и reason_code. Не пишите внутренние имена хостов, токены и личные заметки проверяющего.
{
"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 |
Ваша очередь, затем отображение на два возврата слева |
| Подходит для | Локальной разработки | Размещённого агента | Продукта, которому нужны заявки и аудит |
{
"permissions": {
"allow": ["Bash(npm test *)"],
"ask": ["Bash(git push *)"],
"deny": ["Read(./.env)", "Read(./.env.*)"]
}
}
Инструменты, которые человек обязан увидеть, остаются с effect ask. Если auto считает вызов безопасным, он выполняется до того, как его кто-то увидит. Подробности в предыдущей статье. В продукте источник — ваша policy. Экспорт строки deny для Claude Code — только проекция. Состояние согласования остаётся в очереди.
Проверить три JSON в JSONNote
Перед выпуском прогоните политику, запись очереди и возврат через рабочую область. Все три можно отформатировать, проверить и сравнить в браузере, без живого агента.
-
1
Отформатировать политику
Откройте политику в форматировании JSON. Настройки Claude Code тоже строгий JSON. Комментарий или висячая запятая дадут ошибку при следующем запуске.
-
2
Проверить запись очереди
Вставьте схему ниже в инструмент JSON Schema, затем одну запись согласования. Нет tool_use_id или status вне перечисления — проверка не пройдёт.
-
3
Сравнить изменённые правила
До и после изменения правила используйте JSON Diff, чтобы увидеть, не стало ли
askзначениемallow. Это важнее формулировки. -
4
Проверить возврат
Через проверку JSON убедитесь, что возврат всё ещё корректный JSON и
tool_use_idна месте.
{
"$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 стоит перед выполнением, разобрано в границе безопасности.
Дальше: проверьте форму записи согласования, затем отдайте политику агенту.