Claude Agent SDK 侧的权限怎么配:不是把 CLI 的权限模式照搬一遍
先说这篇要解决的处境
你用 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 模式下把 touch、rm 这类改文件的 shell 命令也送进回调,需要 v2.1.212 或更高;在 v2.1.207 之前,allow 结果如果省略了 updatedInput,Claude Code 会以校验错误拒掉这次工具调用;Python 侧回写权限建议需要 claude-agent-sdk 0.1.80 或更高。
六步评估顺序:回调到底在第几棒
《Configure permissions》页把评估顺序列成了六步。这六步的次序是本文所有结论的地基,先原样复述一遍它们各自在做什么:
| 顺序 | 步骤 | 文档写明的行为 |
|---|---|---|
| 1 | hooks | 最先跑。hook 可以直接拒绝,也可以放行往下走。返回 allow 的 hook 不会跳过下面的 deny 与 ask 规则 |
| 2 | deny 规则 | 来自 disallowed_tools 与 settings.json。命中即阻断,在 bypassPermissions 模式下同样阻断 |
| 3 | ask 规则 | 来自 settings.json。命中则落到 canUseTool 确认,在 bypassPermissions 下也一样 |
| 4 | permission mode | bypassPermissions 批准所有走到这一步的调用;acceptEdits 批准文件操作;plan 把文件编辑与写类 shell 工具无视 allow 规则地送进回调;其余模式继续往下 |
| 5 | allow 规则 | 来自 allowed_tools 与 settings.json。命中即批准 |
| 6 | canUseTool | 前五步都没定论时才调用。在 dontAsk 模式下这一步被跳过,工具直接被拒 |
读这张表的关键不在于记住顺序,而在于看清两件事。
第一,回调是兜底,不是闸门。 第 5 步的 allow 规则一旦命中就直接批准,第 4 步的 acceptEdits / bypassPermissions 也是。文档特别点出,覆盖范围取决于规则的写法:像 Read 或 mcp__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) 规则管着所有写文件的内置工具,包括 Write 和 NotebookEdit;反过来,你写一条 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》页列出的自动批准的文件系统命令是 mkdir、touch、rm、rmdir、mv、cp、sed;而 CLI 的《Choose a permission mode》页额外写明,当 PowerShell 工具启用时,acceptEdits 也会自动批准作用域内路径上的 Set-Content、Add-Content、Clear-Content、Remove-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即可生效;destination为localSettings的建议会把规则写进.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",Bash、Write、Edit 照样全批。要在 bypassPermissions 下挡住特定工具,得用 disallowed_tools。
边界:文档没说、或明说不支持的部分
- subagent 的模式继承:subagent 继承父会话的权限模式。
AgentDefinition里的permissionMode可以覆盖它,但父会话处于bypassPermissions、acceptEdits或auto时不能按 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 文件命令(如cat、head、tail、sed),不适用于自己打开文件的任意子进程,比如一段 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 的公开内容整理。
该产品闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观、操作手感与运行速度的任何描述。
该产品迭代频繁,文中涉及的命令、配置项与默认值随版本变动,请以官方文档最新内容为准。
本文不涉及价格、额度与限流的具体数值,相关信息请以官方定价与用量说明页为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。