Claude Agent SDK 侧的权限怎么配:不是把 CLI 的权限模式照搬一遍

2026-08-18

先说这篇要解决的处境

你用 Claude Agent SDK 写了一个自动化流程,为了保险,认认真真写了一个 canUseTool 回调,把危险操作拦下来。跑起来之后发现:有些工具调用根本没进你的回调,直接就执行了。

这不是 bug。Claude Code 官方文档的《Configure permissions》页(code.claude.com/docs/en/agent-sdk/permissions)把权限评估写成了一条固定顺序的流水线,canUseTool 排在最后一步。前面任何一步给出结论,你的回调就不会被调用——文档用加粗的警告块写明:自动批准的工具永远不会到达 canUseTool,你放在那里的检查对这些工具是被静默绕过的。

所以这篇只落在一件事上:回调在哪一步介入,以及在每一层你能把拒绝的粒度切到多细。

前置条件

在动手之前,有几条边界条件值得先确认,因为它们决定了后面的写法能不能用:

这是 SDK 侧的东西,不是命令行侧的。 CLI 的《Choose a permission mode》页(code.claude.com/docs/en/permission-modes)写明,在终端里可以用 Shift+Tab 循环切换模式;SDK 侧的文档没有给出这条路径,改模式写的是 set_permission_mode()(Python)或 setPermissionMode()(TypeScript)。同一页的表格里还有一行写得很明确:通过 claude -p 或 Agent SDK 启动的会话,内建起始模式是 default——CLI 交互会话那套起始模式判定逻辑,不能直接套到 SDK 上。

Python 侧有一个额外约束。 《Handle approvals and user input》页(code.claude.com/docs/en/agent-sdk/user-input)在 Note 里写明:Python 的 can_use_tool 需要 streaming 模式。通过 query(prompt=generator) 传入有限消息流时,SDK 会在最后一条消息之后关闭输入流,这发生在权限回调被调用之前——除非有已注册的 hook 或进程内 MCP 服务把流撑着。官方示例里放了一个只返回 {"continue_": True}PreToolUse hook 当 workaround。文档同时写明,连接时不带 prompt、用 ClaudeSDKClient.query() 发消息,则不需要这个 hook。

几个有版本门槛的能力(官方文档写明的下限,以最新文档为准):MCP 工具的 _meta["anthropic/requiresUserInteraction"] 标记需要 Claude Code v2.1.199 或更高;plan 模式下把 touchrm 这类改文件的 shell 命令也送进回调,需要 v2.1.212 或更高;在 v2.1.207 之前,allow 结果如果省略了 updatedInput,Claude Code 会以校验错误拒掉这次工具调用;Python 侧回写权限建议需要 claude-agent-sdk 0.1.80 或更高。

六步评估顺序:回调到底在第几棒

《Configure permissions》页把评估顺序列成了六步。这六步的次序是本文所有结论的地基,先原样复述一遍它们各自在做什么:

顺序步骤文档写明的行为
1hooks最先跑。hook 可以直接拒绝,也可以放行往下走。返回 allow 的 hook 不会跳过下面的 deny 与 ask 规则
2deny 规则来自 disallowed_toolssettings.json。命中即阻断,bypassPermissions 模式下同样阻断
3ask 规则来自 settings.json。命中则落到 canUseTool 确认,在 bypassPermissions 下也一样
4permission modebypassPermissions 批准所有走到这一步的调用;acceptEdits 批准文件操作;plan 把文件编辑与写类 shell 工具无视 allow 规则地送进回调;其余模式继续往下
5allow 规则来自 allowed_toolssettings.json。命中即批准
6canUseTool前五步都没定论时才调用。在 dontAsk 模式下这一步被跳过,工具直接被拒

读这张表的关键不在于记住顺序,而在于看清两件事。

第一,回调是兜底,不是闸门。 第 5 步的 allow 规则一旦命中就直接批准,第 4 步的 acceptEdits / bypassPermissions 也是。文档特别点出,覆盖范围取决于规则的写法:像 Readmcp__github__get_issue 这样的裸名字会把该工具的每一次调用都自动批准;而像 Bash(ls *) 这样的带作用域规则只自动批准匹配上的那些调用,其余 Bash 调用仍会落到回调。如果你需要一个对每次工具调用都生效的检查,官方文档给的答案不是回调,而是 PreToolUse hook——它跑在其它所有步骤之前,且 hook 的拒绝在 bypassPermissions 下依然成立。

第二,有几类调用会被强行拉回回调。 文档列了三类:AskUserQuestion、被服务端标了 _meta["anthropic/requiresUserInteraction"] 的 MCP 工具、以及你所在组织在 connector 工具上设为 ask 的那些。这三类即便 allow 规则命中、即便处在 bypassPermissions 模式,也会落到回调;组织 ask 那一类还会给回调带上一个原因字符串 Your organization requires approval for this tool。但在 dontAsk 模式下,这三类不是走回调而是直接被拒,因为该模式从不弹确认。

可拒绝的粒度:规则层能切到哪一刀

回调之外,deny 规则本身就分好几个粒度,文档给了一张对照表,这里只取与”拒绝粒度”直接相关的几行:

写法效果
disallowed_tools=["Bash"]Bash 的工具定义从请求里被移除,Claude 看不到这个工具,也就无从尝试
disallowed_tools=["Bash(rm *)"]Bash 仍然可用,但匹配 rm * 的调用在每一种权限模式下都被拒,包括 bypassPermissions;其它 Bash 调用继续往下走
disallowed_tools=["*"]所有工具定义都被移除。deny 规则支持工具名通配,"*" 匹配所有工具,"mcp__*" 匹配所有 MCP 服务的工具

这里有个容易踩的不对称:deny 规则支持无锚点的通配,allow 规则不支持。 文档写明,allow 规则只允许在字面前缀 mcp__<server>__ 之后使用通配,server 段必须不含通配符,例如 mcp__puppeteer__*mcp__github__get_*;而 allowed_tools=["*"]allowed_tools=["mcp__*"] 这种没锚点的写法会被忽略,启动时给一条警告,什么都不会自动批准。

路径粒度上还有一处反直觉的地方:Edit(path) 规则管着所有写文件的内置工具,包括 WriteNotebookEdit;反过来,你写一条 Write(path) 规则,文件权限检查根本不会去匹配它。想按路径拦写操作,就得写成 Edit(...)

锚点也分两种://path 是文件系统绝对路径,Edit(//secrets/**) 拦的是磁盘上 /secrets 底下的写入;单斜杠的 Edit(/secrets/**) 锚定在规则的来源处——对通过 allowed_tools / disallowed_tools 传进去的规则来说,来源就是会话的工作目录,所以它拦不住磁盘上的 /secrets。这一个斜杠的差别,是拦住了还是没拦住的差别。

Windows 侧的路径怎么写

这一段来自 CLI 侧的权限页(code.claude.com/docs/en/permissions——它的标题与 SDK 那一页同样叫《Configure permissions》,翻文档时容易串页,认 URL 更稳),SDK 那一页并没有单独讲 Windows。该页写明:在 Windows 上,路径在匹配前会被规范化成 POSIX 形式,C:\Users\alice 会变成 /c/Users/alice。所以要匹配某个盘上任意位置的 .env,写法是 //c/**/.env;要跨所有盘符匹配,写 //**/.env

同一页还写明:命令参数里含 Windows 网络路径(UNC,形如 \\server\share\file)时会触发确认,理由是访问网络路径可能把 Windows 凭据发给它指名的主机;PowerShell 工具的命令也走同样的检查。

acceptEdits 在两页里的口径也不一样:SDK 的《Configure permissions》页列出的自动批准的文件系统命令是 mkdirtouchrmrmdirmvcpsed;而 CLI 的《Choose a permission mode》页额外写明,当 PowerShell 工具启用时,acceptEdits 也会自动批准作用域内路径上的 Set-ContentAdd-ContentClear-ContentRemove-Item 及其常见别名。SDK 那一页没有 PowerShell 相关说明,两页的差异到此为止,我们不去猜它在 SDK 下的实际行为。

回调层:允许与拒绝各能做到什么

到了 canUseTool 这一步,《Handle approvals and user input》页写明返回值只有两种形态:

// Allow the tool to execute
return { behavior: "allow", updatedInput: input };

// Block the tool
return { behavior: "deny", message: "User rejected this action" };
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny

# Allow the tool to execute
return PermissionResultAllow(updated_input=input_data)

# Block the tool
return PermissionResultDeny(message="User rejected this action")

粒度就藏在这两个返回值的字段里:

  • 改参数后放行:允许时若返回修改过的 updatedInput / updated_input,工具会用改过的输入执行。文档明说,Claude 看到的是执行结果,不会被告知你改过输入
  • 拒绝时给理由message 会被 Claude 看到,它可能据此调整做法。文档把这一条单列成”建议替代方案”的用法——拦住动作的同时把用户真正想要的写进 message。
  • 记住这次选择:回调的第三个参数带一个 suggestions 数组,里面是现成的 PermissionUpdate 条目。把其中的条目回写到 updatedPermissions 即可生效;destinationlocalSettings 的建议会把规则写进 .claude/settings.local.json,后续会话就不再为匹配的调用弹确认。

回调可以无限期挂起:文档写明执行会一直暂停到你的回调返回,SDK 只有在 query 本身被取消时才结束等待。如果用户可能拖得比你的进程活得还久,文档给的做法是返回 defer 这个 hook 决策,让进程退出、之后从持久化的会话恢复。

想要一个”白名单之外一律硬拒”的锁死配置,文档给的组合是:

const options = {
  allowedTools: ["Read", "Glob", "Grep"],
  permissionMode: "dontAsk"
};

反过来,文档用一个加粗警告点名了一个常见的误配:allowed_tools 约束不了 bypassPermissions 未列出的工具不会被任何 allow 规则匹配,于是落到权限模式那一步,被 bypassPermissions 批准。也就是说 allowed_tools=["Read"] 配上 permission_mode="bypassPermissions"BashWriteEdit 照样全批。要在 bypassPermissions 下挡住特定工具,得用 disallowed_tools

边界:文档没说、或明说不支持的部分

  • subagent 的模式继承:subagent 继承父会话的权限模式。AgentDefinition 里的 permissionMode 可以覆盖它,但父会话处于 bypassPermissionsacceptEditsauto不能按 subagent 覆盖;当 permissions.disableBypassPermissionsMode 关闭了 bypass 模式时,定义里的 permissionMode: "bypassPermissions" 也会被忽略。文档提醒,subagent 的系统提示词与行为约束可能与主 agent 不同,继承 bypassPermissions 等于给它完整的自主系统访问权。
  • AskUserQuestion 在 subagent 里不可用:文档的 Limitations 一节写明,通过 Agent 工具派生的 subagent 目前不支持 AskUserQuestion
  • auto 模式的可用性:SDK 的模式表里对 auto 只写了”由一个模型分类器批准或拒绝权限确认”,并把可用性指向了另一页。这不是一个”配了就有”的模式,用之前请按官方文档核对可用条件。
  • .claude/settings.json 的读取条件:声明式的 allow / deny / ask 规则要在 project 这个 setting source 启用时才会被读到。默认的 query() 选项下它是启用的;但如果你显式设置了 setting_sources(TypeScript 为 settingSources),必须把 "project" 包含进去,否则这些规则不生效。
  • Read / Edit deny 规则管不住间接读写:CLI 侧的权限页(code.claude.com/docs/en/permissions)在警告块里写明,这类规则作用于内置文件工具以及 Claude Code 能识别的 Bash 文件命令(如 catheadtailsed),不适用于自己打开文件的任意子进程,比如一段 Python 或 Node 脚本。文档把这类需求指向了 sandbox。这一条请当作硬边界读,不要把权限规则当成隔离手段。
  • Python 侧的取消信号:回调第三个参数在 TypeScript 里带 signal(一个 AbortSignal),Python 侧文档写明该字段是为将来保留的。

怎么验证配对了

官方文档给了一个不用猜的信号:如果你传入的 canUseTool 回调在这套评估顺序下永远不可能被走到,TypeScript SDK 会在构造 query 时发一次 Node.js 进程警告,警告码是 CLAUDE_SDK_CAN_USE_TOOL_SHADOWED

文档写明有两种配置会触发它:一是 permissionMode: 'bypassPermissions',二是每一条裸名字allowedTools 条目(例如 "Read")。同时写明了不触发的情形:带限定符的条目如 Bash(ls *)、以及 acceptEdits 模式都不会触发,并且来自设置文件的 allow 规则对这个检查不可见——所以没有警告不等于回调一定会被走到,只能说明这两种情况没发生。

监听方式是 process.on('warning', ...) 并按 code 匹配,用来记日志或抑制。文档在这里再次把”要对每次工具调用都生效的门禁”指向 PreToolUse hook。

Python 侧的对应警告,官方文档没有说明这一点。

一个务实的顺序是:先只写回调、不加任何 allow 规则,确认它确实被调用;再一条条加 allow 规则,每加一条都看警告有没有冒出来、以及你原以为会被拦的调用是不是还进得来回调。上面涉及的组合写法为按官方文档中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准。


本文依据 Claude Code 官方文档(code.claude.com/docs)于 2026-08-17 的公开内容整理。 该产品闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观、操作手感与运行速度的任何描述。 该产品迭代频繁,文中涉及的命令、配置项与默认值随版本变动,请以官方文档最新内容为准。 本文不涉及价格、额度与限流的具体数值,相关信息请以官方定价与用量说明页为准。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。