Appearance
第14章 Agent 权限模型设计
Agent 能执行代码、修改文件、访问网络——这些能力让它强大,也让它危险。本章从最小权限原则出发,拆解 Claude Code 的五级权限模型,探讨权限粒度、确认流程、动态升级、持久化策略和审计日志等核心设计决策。
14.1 为什么 Agent 需要权限系统
传统软件的权限模型保护的是"谁能访问什么数据"。Agent 的权限问题本质不同——它保护的是"一个不完全可预测的推理引擎,能对物理世界造成多大影响"。
一个没有权限约束的 Agent 意味着什么?让我们看几个真实场景:
- 模型幻觉导致它认为需要"清理临时文件",执行了
rm -rf /的某个变体 - 模型在解决 bug 时,认为需要安装一个 npm 包,但这个包名是幻觉出来的,实际指向一个恶意包
- 模型在调试网络问题时,将包含 API Key 的配置文件内容发送给了外部服务
- 模型为了"优化性能",修改了数据库配置文件,导致生产数据丢失
这些不是假设。任何使用过 Agent 工具的开发者,都经历过"它差点干了一件蠢事"的时刻。模型的推理能力在统计意义上很强,但它不具备"这件事做错了后果不可逆"的风险意识。它对 rm 和 ls 的调用,在它看来没有本质区别——都只是完成任务的一个步骤。
这就是权限系统存在的根本理由:在模型的能力和它被允许使用的能力之间,建立一道由工程系统维护的屏障。
和传统 RBAC(基于角色的访问控制)不同,Agent 权限系统面临几个独特挑战:
- 行为不可完全预测:你不知道模型下一步要调用什么工具、传什么参数
- 意图需要推断:模型说"我要删除这个文件是为了重建它",你如何判断这是合理操作还是幻觉?
- 粒度极细:不是"能不能访问文件系统"的问题,而是"能不能访问 /etc 目录下的文件"的问题
- 交互式决策:某些权限需要实时向用户确认,这引入了 UX 设计的考量
14.2 Claude Code 的五级权限模型
Claude Code 设计了一个五级权限模型,从最严格到最宽松:
Plan Mode 是最安全的模式。模型只能读取信息、分析代码、给出建议,但不能执行任何修改操作。适合代码审查、架构讨论等不需要实际修改的场景。从 Harness 层面看,这个模式的实现极其简单——不注册任何写入类工具即可。
Default Mode 是大多数用户的日常模式。模型可以调用所有工具,但每次工具调用前都会暂停循环,展示工具名称和参数,等待用户确认。用户可以选择"允许""拒绝"或"允许并记住"。这个模式在安全性和效率之间取得了合理的平衡——对于简单的读取操作,用户快速按下回车;对于危险操作,用户有机会审查。
Auto-Edit Mode 解除了文件编辑的确认要求。模型可以自由地读写文件,但执行 Bash 命令等高风险操作仍需确认。这个模式的设计逻辑是:文件编辑有 Git 兜底,最坏情况下 git checkout 就能恢复;但命令执行的后果可能无法撤销。
Full-Auto Mode 取消所有确认,模型完全自主运行。这个模式适合两种场景:一是 CI/CD 环境中的自动化流水线,二是用户对当前任务有充分信心且希望最大化效率。Claude Code 在启用此模式时会显示醒目的警告。
Custom Rules 是最灵活的模式。用户可以定义精细化的规则,例如"允许所有文件读取,允许编辑 src/ 目录下的文件,禁止编辑 config/ 目录,Bash 命令需要确认但 npm test 自动放行"。这是真正的生产级配置。
这五个级别的设计思路值得深挖。它不是一个简单的"开关",而是一个渐进式信任阶梯。新用户从 Default Mode 开始,每一次交互都在建立对 Agent 行为模式的认知。当用户确认了 100 次 Edit 操作后,自然会想"能不能自动放行这些",于是升级到 Auto-Edit Mode。最终,高度信任 Agent 的用户会使用 Custom Rules 来精确控制权限边界。
14.3 权限粒度:工具、动作、资源三层模型
权限系统的核心设计决策是粒度。粒度太粗,安全形同虚设;粒度太细,用户被确认弹窗淹没。
我把 Agent 权限粒度分为三层:
第一层:per-tool(按工具)
最基础的粒度。允许或禁止某个工具的使用。例如"禁止使用 Bash 工具"或"允许使用 Read 工具"。
typescript
interface ToolPermission {
tool: string // "Bash" | "Edit" | "Read" | ...
allowed: boolean
}这一层实现简单,但远远不够。允许使用 Bash 工具却不区分 ls 和 rm -rf,就像给人一把万能钥匙然后说"只许开你自己的门"。
第二层:per-action(按动作)
在工具内部区分具体动作。对于 Bash 工具,区分不同的命令;对于文件操作工具,区分读、写、创建、删除。
typescript
interface ActionPermission {
tool: string
action: string // "read" | "write" | "execute" | "delete"
allowed: boolean
}Claude Code 在 Bash 工具上做了精细的动作级控制。它维护了一个命令分类体系:
typescript
const commandCategories = {
safe: ["ls", "cat", "grep", "find", "echo", "pwd", "wc"],
moderate: ["npm test", "npm run build", "git status", "git diff"],
dangerous: ["rm", "chmod", "chown", "curl | bash", "eval"],
blocked: ["rm -rf /", ":(){ :|:& };:", "mkfs", "dd if="]
}safe 类命令可以自动放行,moderate 类在宽松模式下放行,dangerous 类始终需要确认,blocked 类直接拒绝。这种分类不是静态配置,而是通过模式匹配动态判断的。
第三层:per-resource(按资源)
最精细的粒度,控制工具可以操作的具体资源。文件系统中的路径、网络中的域名、环境变量中的特定变量——都可以成为资源级权限的控制点。
typescript
interface ResourcePermission {
tool: string
action: string
resource: string // 支持 glob 模式: "src/**/*.ts", "!**/secrets/**"
allowed: boolean
}路径模式匹配是资源级权限最常见的实现方式:
yaml
permissions:
- tool: Edit
allow:
- "src/**"
- "tests/**"
- "docs/**"
deny:
- "**/credentials*"
- "**/.env*"
- "**/secrets/**"
- tool: Bash
allow_commands:
- "npm test*"
- "npm run *"
- "git *"
deny_commands:
- "npm publish*"
- "git push --force*"
- "curl * | bash"三层粒度的选择不是"越细越好"。每增加一层粒度,用户的配置成本和系统的判断开销都在增加。实践中的经验是:工具层做准入控制,动作层做分类管理,资源层只用于高风险场景。
14.4 Allow-list 与 Deny-list:两种策略的博弈
权限系统有两种基本策略:
- Allow-list(白名单):默认禁止一切,只放行明确允许的操作
- Deny-list(黑名单):默认允许一切,只阻止明确禁止的操作
Claude Code 采用了混合策略——宏观上是 Allow-list,微观上用 Deny-list 补充。
宏观层面,每个工具默认需要用户确认才能执行,这本质上是 Allow-list:没有明确授权的操作不会自动执行。但在自动模式下,它切换为 Deny-list 策略:默认允许执行,但维护一个明确的禁止列表。
这种混合策略的逻辑是:
- Allow-list 的问题是枚举不完。合法的 Bash 命令有无限种,你不可能列出所有允许的命令
- Deny-list 的问题是遗漏致命。漏掉一个危险命令,后果可能不可逆
- 所以最佳实践是:在安全模式下用 Allow-list 保底,在自动模式下用 Deny-list 兜底
Deny-list 的设计需要特别谨慎。一个常见的陷阱是只匹配命令前缀:
typescript
// 错误:只检查命令开头
const blocked = ["rm -rf /", "mkfs"]
function isBlocked(cmd: string) {
return blocked.some(b => cmd.startsWith(b))
}
// 绕过方式:
// "cd / && rm -rf ."
// "bash -c 'rm -rf /'"
// "find / -delete"健壮的 Deny-list 需要理解命令的语义,而不是简单的字符串匹配。Claude Code 的做法是先对命令进行解析——拆分管道、识别子 shell、展开别名——然后对每个原子命令进行检查。
14.5 动态权限升级:从怀疑到信任
静态权限配置无法覆盖所有场景。一个更自然的模式是动态权限升级:Agent 从最小权限开始,根据行为表现和用户反馈逐步获得更多权限。
这个机制有两个维度:
单次会话内的升级:
用户:"帮我重构 auth 模块"
第 1 轮:Agent 使用 Read/Grep 分析代码结构 → 自动放行
第 2 轮:Agent 想用 Edit 修改 src/auth/login.ts → 请求确认
用户:允许,并记住"允许编辑 src/auth/**"
第 3 轮:Agent 编辑 src/auth/session.ts → 自动放行(已授权)
第 4 轮:Agent 想运行 npm test → 请求确认
用户:允许,并记住"允许 npm test"
第 5 轮:Agent 修改另一个 auth 文件并运行测试 → 全部自动放行这就是"渐进式信任"的实际体验。用户不需要提前配置所有权限,也不需要每次都确认相同类型的操作。系统会学习用户的授权模式。
跨会话的信任积累:
用户在项目级配置文件(如 .claude/settings.json)中记录的权限规则会持久化。经过多次会话后,这些规则逐渐形成一个针对特定项目的权限画像:
json
{
"permissions": {
"allow": [
"Edit(src/**)",
"Edit(tests/**)",
"Bash(npm test*)",
"Bash(npm run build)",
"Bash(git status)",
"Bash(git diff*)"
],
"deny": [
"Bash(npm publish*)",
"Bash(git push*)",
"Edit(**/.env*)"
]
}
}动态权限升级的关键设计约束是单向阶梯——权限只能升不能降(在单次会话内)。如果系统检测到 Agent 执行了一个可疑操作后自动降低权限,会导致用户体验的混乱。更好的做法是:可疑操作触发一次确认,但不改变已有的权限授予。
14.6 用户确认流程的设计
确认流程是权限系统的"用户界面"。设计得好,用户感觉安全又高效;设计得差,用户要么被烦死,要么盲目点"允许"——两种情况都违背了权限系统的初衷。
一个好的确认提示需要回答三个问题:
- Agent 要做什么? 工具名称 + 参数的清晰展示
- 为什么要做? Agent 的推理过程(上下文)
- 风险是什么? 操作的潜在后果
Claude Code 的确认弹窗遵循这个结构:
╭─────────────────────────────────────────────────╮
│ Claude wants to run: Bash │
│ │
│ Command: npm install lodash@4.17.21 │
│ │
│ [Allow] [Deny] [Allow Always for this project]│
╰─────────────────────────────────────────────────╯几个关键的 UX 设计决策:
展示完整命令,而非摘要。 用户需要看到 rm -rf ./dist 而不是"删除某个目录"。摘要可能隐藏关键细节。
提供"记住"选项。 "Allow Always"让用户一次授权,永久生效(在项目范围内)。这大幅减少了重复确认的疲劳。
对危险操作使用视觉警告。 当命令匹配高危模式时(如包含 rm、chmod 777),确认提示应该用红色高亮或额外的警告文本。
批量确认。 当模型在一个循环中连续执行多个同类操作时,提供"允许接下来 N 个同类操作"的选项,避免用户变成点击机器。
有一个反直觉的观察:确认疲劳比没有确认更危险。 当用户连续确认了 20 个无害操作后,第 21 个危险操作也会被条件反射般地确认。这就是为什么权限系统不应该对所有操作都弹确认——只对真正需要人类判断的操作弹出,其他的要么自动放行,要么自动拒绝。
14.7 权限持久化:会话、项目、全局三层
权限规则需要在不同范围内持久化:
会话级(Session): 存在内存中,会话结束即消失。"允许这次编辑 package.json"就是会话级权限。适合一次性任务中的临时授权。
项目级(Project): 存在项目目录的配置文件中(如 .claude/settings.json)。"在这个项目中,允许运行 npm test"是项目级权限。它跟随项目仓库,团队成员可以共享。
全局级(Global): 存在用户主目录的配置文件中(如 ~/.claude/settings.json)。"在所有项目中,禁止运行 rm -rf"是全局级权限。它代表用户的安全底线。
三层之间的优先级遵循一个简单规则:deny 优先于 allow,范围小的优先于范围大的。
全局 deny "rm -rf *" → 最终结果:deny(全局 deny 不可被覆盖)
项目 allow "rm -rf dist" → 被全局 deny 覆盖
会话 allow "rm -rf dist" → 被全局 deny 覆盖
全局 allow "npm test" → 基础权限
项目 deny "npm test" → 最终结果:deny(项目级覆盖全局级)这个优先级模型的核心思想是:安全约束只能收紧,不能放松。 全局设置的 deny 规则是不可被项目或会话覆盖的安全底线。项目可以在全局允许的范围内进一步收紧,但不能突破全局设置的禁区。
项目级权限的一个重要考量是是否纳入版本控制。如果纳入,团队共享同一套权限规则,新成员不需要重新配置;如果不纳入,每个开发者可以有自己的权限偏好。Claude Code 的做法是将权限配置文件放在 .claude/ 目录下,由团队自行决定是否将其加入 .gitignore。
14.8 最小权限原则的 Agent 化
最小权限原则(Principle of Least Privilege)在传统安全领域是常识:每个主体只应获得完成其任务所需的最小权限集。但在 Agent 场景中,这个原则的应用远比传统系统复杂。
传统系统中,一个服务的权限在部署时确定,运行期间不变。Agent 的任务是动态的——同一个 Agent,这一刻在读代码,下一刻在执行测试,再下一刻在修改配置。它需要的权限随任务阶段而变化。
最小权限的 Agent 化实现需要三个机制:
任务感知的权限推断: 根据用户的初始请求,推断这个任务可能需要的权限范围。"帮我看看这段代码有什么问题"只需要读取权限;"帮我修复这个 bug"需要读取 + 编辑权限;"帮我部署到 staging"需要几乎所有权限。
阶段性权限授予: 不在任务开始时就授予所有可能需要的权限,而是按阶段释放。分析阶段只给读取权限,修改阶段增加编辑权限,验证阶段增加执行权限。
权限自动回收: 当一个子任务完成后,为其临时授予的权限应当回收。模型不应该因为在任务 A 中获得了 Bash 执行权限,就在无关的任务 B 中继续持有这个权限。
在实践中,完全自动化的最小权限实现仍然很困难。目前更可行的方式是半自动的——系统基于任务类型提供权限建议,用户一键确认或调整。
14.9 审计日志:每个决策都有据可查
权限系统的最后一环是审计。没有审计的权限系统就像没有监控的防火墙——你知道有规则在运行,但不知道它们是否在起作用。
一个完整的权限审计日志应该记录:
typescript
interface PermissionAuditEntry {
timestamp: string
sessionId: string
tool: string // 请求的工具
action: string // 请求的动作
resource: string // 操作的资源
decision: "allow" | "deny" | "ask_user"
decisionSource: string // "global_deny" | "project_allow" | "user_confirmed" | ...
userResponse?: string // 如果是 ask_user,用户的响应
modelReasoning?: string // 模型为什么要执行这个操作
}审计日志的价值不仅在于事后追查,更在于权限规则的优化。分析日志可以发现:
- 哪些操作被频繁确认后允许?它们可能应该加入 allow-list
- 哪些操作被频繁拒绝?它们可能应该加入 deny-list
- 哪些确认被用户秒过?可能存在确认疲劳
- 模型是否尝试过超出任务范围的操作?可能需要调整 prompt
这些数据驱动的洞察,是持续改进权限策略的基础。
14.10 横向对比:Claude Code、Cursor、Devin
不同的 Agent 产品对权限模型的设计哲学截然不同,反映了它们对"Agent 自主性"的不同定位。
Claude Code 的权限模型是用户主权型。用户对每个操作有最终控制权,系统提供多级灵活配置。它假设用户是有经验的开发者,愿意也有能力管理权限规则。这种设计的优势是安全性上限高,劣势是新用户的上手成本也高。
Cursor 的权限模型是场景隔离型。它把 Agent 的能力按功能区域隔离——代码编辑在编辑器内完成,终端命令在独立面板中执行,每个区域有自己的权限规则。这种设计的优势是利用了 IDE 已有的安全边界(编辑器中的操作天然可撤销),劣势是跨区域协作时权限管理变得碎片化。
Devin 的权限模型是沙箱型。它给 Agent 一个完整的隔离环境(虚拟机或容器),Agent 在沙箱内拥有几乎完全的自由。安全边界不在操作级别,而在环境级别——沙箱内怎么折腾都行,出不了沙箱就行。这种设计的优势是 Agent 的自主性最高,执行效率最好;劣势是沙箱的构建成本高,且沙箱内的错误仍然需要恢复。
三种模型的对比:
| 维度 | Claude Code | Cursor | Devin |
|---|---|---|---|
| 控制粒度 | 工具/动作/资源 三层 | 功能区域级 | 环境级 |
| 用户参与 | 高(可配置为低) | 中等 | 低 |
| 安全上限 | 最高 | 中等 | 取决于沙箱质量 |
| 自主性 | 低→高可调 | 中等 | 高 |
| 配置成本 | 高 | 低 | 低(由平台承担) |
没有"最好"的权限模型,只有最适合特定场景的模型。本地开发工具更适合 Claude Code 的用户主权型——开发者需要精细控制。云端自动化平台更适合 Devin 的沙箱型——隔离环境比操作级审批更高效。IDE 集成场景更适合 Cursor 的场景隔离型——利用已有的安全基础设施。
14.11 设计你自己的权限模型
如果你正在构建一个 Agent 系统,以下是我总结的权限模型设计清单:
- 确定安全底线:哪些操作在任何情况下都不允许?把它们放入全局 deny-list
- 按工具分类风险:读取类工具通常安全,可以默认放行;写入类需要确认;执行类需要更严格的控制
- 设计确认流程时考虑疲劳:如果用户每分钟需要确认超过 3 次,你的默认权限太严格了
- 提供"记住"机制:每次确认都应该提供持久化选项,避免重复劳动
- deny 优先于 allow:在优先级冲突时,永远选择更安全的决策
- 记录一切:审计日志是优化权限策略的唯一可靠数据来源
- 提供合理的预设:不要让用户从零开始配置权限。根据典型使用场景提供预设模板
- 权限配置本身需要保护:修改权限规则的操作,应该有独立的确认机制
权限模型不是一次设计完就不动的。它应该随着用户的使用模式、模型能力的进化、攻击手段的演变而持续迭代。一个好的权限系统,今天看起来略显繁琐,明天可能救你一命。