Skip to content

第14章 Agent 权限模型设计

Agent 能执行代码、修改文件、访问网络——这些能力让它强大,也让它危险。本章从最小权限原则出发,拆解 Claude Code 的五级权限模型,探讨权限粒度、确认流程、动态升级、持久化策略和审计日志等核心设计决策。

14.1 为什么 Agent 需要权限系统

传统软件的权限模型保护的是"谁能访问什么数据"。Agent 的权限问题本质不同——它保护的是"一个不完全可预测的推理引擎,能对物理世界造成多大影响"。

一个没有权限约束的 Agent 意味着什么?让我们看几个真实场景:

  • 模型幻觉导致它认为需要"清理临时文件",执行了 rm -rf / 的某个变体
  • 模型在解决 bug 时,认为需要安装一个 npm 包,但这个包名是幻觉出来的,实际指向一个恶意包
  • 模型在调试网络问题时,将包含 API Key 的配置文件内容发送给了外部服务
  • 模型为了"优化性能",修改了数据库配置文件,导致生产数据丢失

这些不是假设。任何使用过 Agent 工具的开发者,都经历过"它差点干了一件蠢事"的时刻。模型的推理能力在统计意义上很强,但它不具备"这件事做错了后果不可逆"的风险意识。它对 rmls 的调用,在它看来没有本质区别——都只是完成任务的一个步骤。

这就是权限系统存在的根本理由:在模型的能力和它被允许使用的能力之间,建立一道由工程系统维护的屏障。

和传统 RBAC(基于角色的访问控制)不同,Agent 权限系统面临几个独特挑战:

  1. 行为不可完全预测:你不知道模型下一步要调用什么工具、传什么参数
  2. 意图需要推断:模型说"我要删除这个文件是为了重建它",你如何判断这是合理操作还是幻觉?
  3. 粒度极细:不是"能不能访问文件系统"的问题,而是"能不能访问 /etc 目录下的文件"的问题
  4. 交互式决策:某些权限需要实时向用户确认,这引入了 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 工具却不区分 lsrm -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 策略:默认允许执行,但维护一个明确的禁止列表。

这种混合策略的逻辑是:

  1. Allow-list 的问题是枚举不完。合法的 Bash 命令有无限种,你不可能列出所有允许的命令
  2. Deny-list 的问题是遗漏致命。漏掉一个危险命令,后果可能不可逆
  3. 所以最佳实践是:在安全模式下用 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 用户确认流程的设计

确认流程是权限系统的"用户界面"。设计得好,用户感觉安全又高效;设计得差,用户要么被烦死,要么盲目点"允许"——两种情况都违背了权限系统的初衷。

一个好的确认提示需要回答三个问题:

  1. Agent 要做什么? 工具名称 + 参数的清晰展示
  2. 为什么要做? Agent 的推理过程(上下文)
  3. 风险是什么? 操作的潜在后果

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"让用户一次授权,永久生效(在项目范围内)。这大幅减少了重复确认的疲劳。

对危险操作使用视觉警告。 当命令匹配高危模式时(如包含 rmchmod 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 CodeCursorDevin
控制粒度工具/动作/资源 三层功能区域级环境级
用户参与高(可配置为低)中等
安全上限最高中等取决于沙箱质量
自主性低→高可调中等
配置成本低(由平台承担)

没有"最好"的权限模型,只有最适合特定场景的模型。本地开发工具更适合 Claude Code 的用户主权型——开发者需要精细控制。云端自动化平台更适合 Devin 的沙箱型——隔离环境比操作级审批更高效。IDE 集成场景更适合 Cursor 的场景隔离型——利用已有的安全基础设施。

14.11 设计你自己的权限模型

如果你正在构建一个 Agent 系统,以下是我总结的权限模型设计清单:

  1. 确定安全底线:哪些操作在任何情况下都不允许?把它们放入全局 deny-list
  2. 按工具分类风险:读取类工具通常安全,可以默认放行;写入类需要确认;执行类需要更严格的控制
  3. 设计确认流程时考虑疲劳:如果用户每分钟需要确认超过 3 次,你的默认权限太严格了
  4. 提供"记住"机制:每次确认都应该提供持久化选项,避免重复劳动
  5. deny 优先于 allow:在优先级冲突时,永远选择更安全的决策
  6. 记录一切:审计日志是优化权限策略的唯一可靠数据来源
  7. 提供合理的预设:不要让用户从零开始配置权限。根据典型使用场景提供预设模板
  8. 权限配置本身需要保护:修改权限规则的操作,应该有独立的确认机制

权限模型不是一次设计完就不动的。它应该随着用户的使用模式、模型能力的进化、攻击手段的演变而持续迭代。一个好的权限系统,今天看起来略显繁琐,明天可能救你一命。

基于 VitePress 构建