博客 • 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。

返回博客