权限规则与审批模式
allow / ask / deny 规则、权限模式与沙箱如何在 Harness 里配合把关。
一句话理解:一个工具请求要过三道关:权限规则按 deny → ask → allow 的固定顺序匹配,决定“这类动作能不能做”;权限模式决定“规则没覆盖到的动作默认怎么办”;沙箱决定“放行之后最多能碰到什么”。三者叠加,才既不烦人,也不失控。
几乎每个 Agent 产品都会经历这样一个阶段:一开始每个动作都弹窗确认,用户被问烦了,开始闭着眼睛点“允许”,甚至直接打开“跳过所有检查”;出过一次事故之后,团队又把确认收紧,Agent 重新变得寸步难行。问题不在于该松还是该紧,而在于 只有一个开关。
安全模块已经讨论过哪些操作该自动执行、哪些该让人确认(操作分级、Human-in-the-loop),沙箱的隔离原理见 沙箱。这一课讲 Harness 怎么把这些原则 写成配置、按顺序执行。例子以 Claude Code 和 Codex 的官方文档为准,最后再落到一个自研的客服工单 Agent 上。
三道关,各管一件事
| 关卡 | 回答的问题 | 在什么时候起作用 | 例子 |
|---|---|---|---|
| 权限规则 | 这一类具体动作能不能做? | 工具执行前,按调用内容匹配 | 允许 npm run test,询问 git push,禁止读 .env |
| 权限模式 | 规则没覆盖的动作,默认问不问人? | 规则之后,作为整体基调 | 自动接受文件编辑;无人值守时一律拒绝 |
| 沙箱 | 放行之后,命令最多能读写哪里、连哪里? | 命令运行时,由操作系统强制 | 只能写项目目录,只能连包管理器 |
前两关在执行前看“字面上要做什么”,第三关在运行时看“实际做了什么”。上一课 工具运行时 流水线里的“权限判定”,就是前两关;沙箱则包在“执行”这一步外面。
规则怎么写:以 Claude Code 为例
Claude Code 的规则写在 settings.json 的 permissions 里,分 allow、ask、deny 三个列表,每条规则的格式是 工具名 或 工具名(限定条件):
Bash(npm run *):匹配以npm run开头的命令,*代表任意文本。Read(./.env)、Edit(/src/**):文件路径用 gitignore 语法;//开头是文件系统绝对路径,~/是用户目录,单个/开头是相对配置来源的路径。WebFetch(domain:example.com):按域名匹配网页抓取。mcp__github__get_*:匹配名为github的 MCP Server 中以get_开头的工具。
下面是一个 Coding Agent 项目的配置示例(放在项目的 .claude/settings.json):
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash(npm run test *)",
"Bash(npm run lint)",
"Bash(git diff *)",
"Bash(git commit *)"
],
"ask": [
"Bash(git push *)",
"Bash(npm install *)"
],
"deny": [
"Bash(git push --force *)",
"Bash(curl *)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
},
"sandbox": {
"enabled": true,
"network": {
"allowedDomains": ["registry.npmjs.org"]
}
}
}
匹配顺序:deny 先查,第一个命中说了算
Claude Code 的规则 按 deny → ask → allow 的顺序检查,第一个命中的决定结果,写得多具体都不改变这个顺序。这带来两个反直觉的后果:
- allow 不能在 deny 里“开例外”。有了
Bash(aws *)这条 deny,再写一条更具体的 allowBash(aws s3 ls)也没用。 - ask 压过 allow。同一个调用同时命中 ask 和 allow,照样要问。
用上面的配置走几个请求:
| 请求 | 命中 | 结果 |
|---|---|---|
git push --force origin main |
deny(也命中 ask,但 deny 先查) | 拒绝 |
git push origin main |
ask | 询问用户 |
npm run test -- auth |
allow | 直接执行 |
编辑 src/auth.ts |
无规则,由 acceptEdits 模式放行 |
直接执行 |
cat .env |
deny Read(./.env)(对 cat 这类命令同样生效) |
拒绝 |
node scripts/sync.js |
无规则;沙箱开启时,沙箱内命令默认自动放行 | 在沙箱里执行,访问未放行的域名时再询问 |
多个配置层级之间也是“拒绝优先”:用户、项目、本地、命令行参数和企业托管配置里,任何一层的 deny 都不能被其他层的 allow 推翻,托管配置的优先级最高。另外,项目配置里的 allow 规则会给 Agent 放权,只有在用户信任这个目录之后才生效,而 deny 和 ask 只会收紧权限,不受这个限制。
把规则、模式、询问和沙箱串起来,就是一个工具请求的完整判定流程。Claude Agent SDK 的文档给出了固定顺序:Hooks → deny 规则 → ask 规则 → 权限模式 → allow 规则 → 询问回调:
读图时注意两点。第一,被自动放行的调用不会走到“询问用户”这一步:如果你把自定义审批逻辑写在询问回调里,模式或 allow 规则放行的调用会悄悄绕过它;必须对每个调用生效的检查,要放在最前面的 Hooks 里(下一课展开)。第二,被规则拒绝并不会中断循环,拒绝原因会作为工具结果交给模型,它通常会换一种做法,或如实报告做不了。
权限模式:没被规则覆盖时的默认态度
规则只能覆盖你想得到的动作,其余的交给模式。Claude Code 目前有六种模式,本质上是在选 “规则没覆盖的动作,由谁来审批”:
| 模式 | 不问就能做的事 | 适合 |
|---|---|---|
default(界面上叫 Manual) |
只有读取 | 敏感工作、陌生代码 |
acceptEdits |
读取、工作目录内的文件编辑和常见文件命令 | 边改边审的日常开发 |
plan |
读取和只读命令,不改源码 | 先探索、出方案,再动手 |
auto |
几乎全部,由后台分类器做安全检查 | 长任务、减少打断 |
dontAsk |
读取和预先允许的工具,其余一律拒绝 | CI、脚本等无人值守场景 |
bypassPermissions |
全部 | 只在隔离的容器或虚拟机里用 |
几个细节决定了模式能不能放心用:
- 有些动作任何模式都不自动放行:命中显式 ask 规则的调用、需要用户交互的工具,以及对关键路径的删除。写入
.git、.claude这类受保护路径,除bypassPermissions外也都不会被直接放行(auto模式下交给分类器判断)。 bypassPermissions下,allow 列表不再限制任何东西。Agent SDK 文档特别提醒:只列了Read的allowed_tools,配上这个模式照样放行所有工具;想禁用某个工具,只能用 deny。- 无人值守用
dontAsk,而不是指望“没人点确认”:它把所有“本该询问”的调用直接转成拒绝,工具边界一目了然。 - 子 Agent 默认继承父会话的模式,父会话开了
bypassPermissions,子 Agent 也一样。
沙箱:放行之后的最后一道墙
权限规则只能看命令的字面。官方文档说得很坦率:Bash(curl *) 这条 deny 拦得住 curl https://...,却拦不住 /usr/bin/curl 或 sh -c 'curl ...'。所以规则 不是安全边界,真正兜底的是沙箱:它由操作系统限制命令及其子进程能读写的路径和能连接的域名,不管模型写成什么形式都成立。
两者在 Claude Code 里这样配合:开启沙箱后,默认 沙箱内的 Bash 命令自动放行,沙箱边界替代了逐条确认;但 deny 规则、Bash(git push *) 这种针对具体命令的 ask 规则照样生效。命令在沙箱里跑不通时,Agent 可以申请脱离沙箱重试,这会重新走正常的权限流程;把 allowUnsandboxedCommands 设为 false 可以关掉这个逃生口。
Codex 走的是另一条路:先划沙箱,越界才审批。
# ~/.codex/config.toml
sandbox_mode = "workspace-write" # 另有 read-only 与 danger-full-access
approval_policy = "on-request" # 沙箱内自动执行,越界(写工作区外、联网)才请示
对于要在沙箱外运行的具体命令,Codex 再用规则文件细分:prefix_rule 按命令前缀匹配,决定是 allow、prompt 还是 forbidden;多条规则同时命中时取最严格的(forbidden 高于 prompt 高于 allow)。
落到自研 Harness:客服工单 Agent
不做 Coding Agent,也可以照搬这套结构。假设客服工单 Agent 有查询工单、搜索知识库、回复客户、退款、删除工单五个工具,先定规则:
| 列表 | 规则 | 理由 |
|---|---|---|
| allow | 查询工单、搜索知识库 | 只读,无副作用 |
| ask | 回复客户、退款 | 对外发送、涉及资金,需要坐席确认 |
| deny | 删除工单 | 不可逆,Agent 不应该有这个能力 |
再把判定写成上一课 run_one 里调用的 policy.decide(伪代码,不对应任何特定 SDK):
# 伪代码:与 Claude Code 相同的判定顺序
def decide(tool, args, session):
call = tool.describe(args) # 例如 "refund(order=A1, amount=300)"
if matches(RULES["deny"], call): # ① deny:命中即拒
return Decision("deny", reason="命中禁止规则:Agent 不能删除工单")
if matches(RULES["ask"], call): # ② ask:命中必须问人
return ask_or_deny(call, session)
if session.mode == "readonly" and tool.writes: # ③ 模式:只读模式下写操作一律问人
return ask_or_deny(call, session)
if matches(RULES["allow"], call): # ④ allow:命中即放行
return Decision("allow")
return ask_or_deny(call, session) # ⑤ 兜底:问人
def ask_or_deny(call, session):
if session.unattended: # 夜间批处理没人可问:直接拒绝(类似 dontAsk)
return Decision("deny", reason="无人值守,未预先允许")
return session.request_approval(call) # 推到坐席的审批界面,等待确认
审批界面怎么设计才不让人闭眼点“同意”,见 审批交互;工具凭据和作用域本身的收敛,见 最小权限与凭据管理。
常见误区
- “再写一条更具体的 allow,就能给 deny 开个口子。” 规则按 deny → ask → allow 检查,具体程度不影响顺序。真需要例外,应该缩小 deny 的范围。
- “写了
Bash(rm *)的 deny,就不会删文件了。” 规则只匹配命令字面,换个写法就能绕过。要硬边界,用沙箱限制可写路径。 - “开了沙箱,就可以放心用跳过检查的模式。” 沙箱只管 Shell 命令的文件和网络边界,管不了文件编辑工具、MCP 工具,也管不了在允许范围内做错事。
bypassPermissions仍然只该出现在一次性的隔离环境里。
产品经理检查清单
- 能列出这个 Agent 的 deny 清单吗?每条都是“无论如何都不能做”的动作吗?
- ask 清单里的动作,用户每天会被问多少次?会不会问到麻木?
- 规则没覆盖的动作,默认模式是什么?交互场景和无人值守场景是否用了不同的模式?
- 哪些限制只写在规则里、没有沙箱兜底?被绕过的后果是什么?
- 规则分几层(企业、项目、个人)?谁能改哪一层?改动有没有记录?
- 被拒绝时,模型收到的原因能帮它换一条合规路径吗?
练习
为一个你熟悉的代码仓库写一份权限配置(约 45 分钟):
- 写出 allow、ask、deny 各至少三条规则,选一个默认模式,决定是否开启沙箱以及允许哪些域名。
- 列出 8 个 Agent 可能发出的请求(包括
git push -f、/bin/rm -rf build、读取.env.local、安装新依赖等),先预测每个请求的结果和命中的规则,再说明哪几个是“规则挡不住、要靠沙箱”的。 - 如果手边有 Claude Code,把配置放进项目并用
/permissions查看,逐条验证你的预测。
延伸阅读
- Claude Code:Configure permissions:规则语法、匹配顺序、配置层级与沙箱的关系。
- Claude Code:Choose a permission mode:六种模式、受保护路径与任何模式都不自动放行的动作。
- Claude Agent SDK:Configure permissions:Hooks、规则、模式与询问回调的完整判定顺序。
- Claude Code:Configure the sandboxed Bash tool:沙箱模式、网络白名单与脱离沙箱重试。
- Codex:Agent approvals & security:沙箱模式与审批策略的组合。
- Codex:Rules:
prefix_rule与最严格者胜的规则。
下一步
规则和模式是声明式的,写不出“退款金额超过阈值才要主管审批”这种带逻辑的判断。下一课「Hooks 与生命周期」讲如何在工具调用前后插入一段一定会执行的代码,补上这最后一块。