博客 • AI / Agent

Claude Agent 权限控制怎么实现?从 Permission Policy、MCP Tool Call 到 JSON 审批流程完整解析

Claude Agent 的权限控制,是插在模型提出的 Tool Call 和真正执行之间的一层 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 先拦

    规则命中就拦住,bypassPermissions 模式也一样。只写工具名的 deny(例如整条 Bash)会在这一步之前把工具从上下文里拿掉。

  2. 2
    ask 进入回调

    规则命中就交给 canUseTool,即便开了绕过模式。在 dontAsk 里没有提示,命中的调用直接拒绝,不再回调。

  3. 3
    allow 不再询问

    规则命中就直接执行,canUseTool 不会被调用。只把工具名放进允许名单,效果相同,除非另有 ask 规则把它拉回回调。

  4. 4
    都没命中

    前面都没命中时,default 模式才把调用交给 canUseTool。dontAsk 则直接拒绝,不再回调。

因此审批队列只能接住还没被放行的调用。若要对每一次调用都记账,用 PreToolUse hook。它跑在其余检查之前,可以允许、拒绝或改参数,但盖不过已经命中的 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 判成 deny 时,客户端不能用确认事件推翻。队列不要做成第二条强制放行通道。

Permission Policy JSON 怎么写

只跑 Claude Code 时,官方 settings.json 里的 allow / ask / deny 字符串就够。产品里的 Agent 还要一份能版本化、能 diff、能用 Schema 校验的文档。下面这份是你的应用契约,不是 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 Tool Call 怎么对上规则

模型给出的调用至少带类型、id、工具名和 input。字段名以 Messages API 的 tool_use 为准。MCP 工具名是服务器名和工具名拼出来的全名。

一次对着受保护分支开拉取请求的调用
{
  "type": "tool_use",
  "id": "toolu_01AbC",
  "name": "mcp__github__create_pull_request",
  "input": {
    "base": "main",
    "title": "Retry failed checks",
    "head": "fix/retry"
  }
}
  1. 1
    先校验形状

    用该工具自己的 JSON Schema 校验 input。校验失败:不入队、不调用 MCP,直接 deny,reason_code 用 schema_invalid。

  2. 2
    再按 deny、ask、allow 匹配

    上面这条命中 ask-pr-main,因为 base 落在 enum 里。同一 effect 内才看文件顺序。

  3. 3
    不要只写工具名就 allow

    若 allow 规则没有 input 约束,base 为 main 也会被放行。参数约束是权限的一部分,不是 Schema 的重复。

只做相等和 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 里自带的用户字段。

拒绝和同意怎么写回

决定一旦进入终态,必须写回同一次 tool_use_id。漏写的结果是会话停在等待,模型既不能继续,也可能在下一轮用新的 id 再提一次。

你的决定 Agent SDK 回调 托管工具停在 ask 自定义工具
allow { behavior: "allow", updatedInput } result: "allow" 你执行成功后,回自定义工具结果
deny { behavior: "deny", message } result: "deny" + deny_message 带错误标记的自定义工具结果,写上 reason_code

见 官方用户输入文档。文档写明,TypeScript 的 allow 要带 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
}

只有 allow 之后才调用 MCP。deny 和 expired 不调用。策略引擎不负责先执行、再看人批不批。

和 settings、auto 怎么分工

三套规则回答的不是同一件事。不要各写各的名单,再指望它们自动对齐。

Claude Code settings Managed Agents 你的 policy 和队列
规则形状 Tool 加说明的字符串 always_allow / always_ask / auto effect,加上工具名和可选的 input 约束
谁评估 Claude Code,顺序是 deny、ask、allow 平台。auto 按这一次调用评估 你的代码。顺序与官方对齐
人批落在哪 交互提示,或 canUseTool user.tool_confirmation 你的队列,再映射到左边两种回写
适合 本机编码 托管 Agent 产品要留下审批单和审计
Claude Code 的权限规则。这是官方 settings 形状,不是上面那份应用策略。
{
  "permissions": {
    "allow": ["Bash(npm test *)"],
    "ask": ["Bash(git push *)"],
    "deny": ["Read(./.env)", "Read(./.env.*)"]
  }
}

必须人看的工具,effect 保持 ask。auto 在判定安全时会在人看见之前执行,细节见 上一篇。产品里以你的 policy 为源;导出成 Claude Code 的 deny 字符串只是一种投影,审批状态仍以队列为准。

在 JSONNote 里检查这三份 JSON

上线前用工作区把策略、队列记录和回写各过一遍。这三份都能在浏览器里格式化、校验和对比,不必先接到线上 Agent。

  1. 1
    格式化策略

    用 JSON 格式化 打开 policy。Claude Code 的 settings 也是严格 JSON,注释和尾逗号都会在下次启动时报错。

  2. 2
    校验队列记录

    把下面的 Schema 贴进 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 够不够?

不够。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:你写的 policy、ask 时落下的队列记录、终态时写回的决定。匹配顺序跟官方一样,deny 优先。Schema 只管形状。人还没批,或者已经过期,都不调用 MCP。

直接结论:默认用 ask 或 deny;allow 必须写到工具名,需要时再加参数;每一条 tool_use_id 只执行一次,并且一定写回。

MCP 工具谁负责声明、谁负责执行,见 MCP 安全。为什么 JSON 要挡在执行前面,见 安全边界。

下一步:先校验审批记录的形状,再把策略交给 Agent。

返回博客