博客 • 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:策略、審批隊列、拒絕回寫。自定義工具,以及回調裏纔看得到的調用,都走這裏。
本文將介紹:
- 官方評估走到哪一步,
canUseTool纔會被調用 - Permission Policy JSON 的字段、默認值和匹配順序
- MCP Tool Call 如何先過 Schema,再對上規則
- 審批隊列記錄哪些字段,過期以後怎麼處理
- 同意和拒絕怎樣寫回,避免同一次調用執行兩遍
先記住這一點:把某條工具寫成 allow,審批隊列就看不見它。canUseTool 不會在已經放行的調用上觸發。下面按「接入點 → 策略 JSON → 隊列 → 回寫」拆你要自己留下的文檔。
你接在官方評估的哪一步
Agent SDK 和 Claude Code 的評估順序是固定的。依據見 Agent SDK 權限 和 Claude Code 權限。規則先看 deny,再看 ask,再看 allow。更具體的 allow 不能蓋過已經命中的 deny。
-
1
deny 先攔
規則命中就攔住,
bypassPermissions模式也一樣。只寫工具名的 deny(例如整條 Bash)會在這一步之前把工具從上下文裏拿掉。 -
2
ask 進入回調
規則命中就交給
canUseTool,即便開了繞過模式。在dontAsk裏沒有提示,命中的調用直接拒絕,不再回調。 -
3
allow 不再詢問
規則命中就直接執行,
canUseTool不會被調用。只把工具名放進允許名單,效果相同,除非另有 ask 規則把它拉回回調。 -
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 的請求體。
{
"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
先校驗形狀
用該工具自己的 JSON Schema 校驗 input。校驗失敗:不入隊、不調用 MCP,直接 deny,reason_code 用
schema_invalid。 -
2
再按 deny、ask、allow 匹配
上面這條命中
ask-pr-main,因爲base落在 enum 裏。同一 effect 內纔看文件順序。 -
3
不要只寫工具名就 allow
若 allow 規則沒有 input 約束,base 爲 main 也會被放行。參數約束是權限的一部分,不是 Schema 的重複。
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_code 用approval_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 裏自帶的用戶字段。
拒絕和同意怎麼寫回
決定一旦進入終態,必須寫回同一次 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。
{
"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
}
只有 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 | 產品要留下審批單和審計 |
{
"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
格式化策略
用 JSON 格式化 打開 policy。Claude Code 的 settings 也是嚴格 JSON,註釋和尾逗號都會在下次啓動時報錯。
-
2
校驗隊列記錄
把下面的 Schema 貼進 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 夠不夠?
不夠。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。