第 45 章:安全与便利的平衡

源码验证日期:2026-05-15,基于 commit
0d81bb6
想象一个场景:你让 Claude Code 帮你重构项目,它开始执行 rm -rf src/ 来”清理旧代码”。你的整个源码目录消失了。
这就是 AI 编程助手面临的核心安全挑战。它有执行能力——可以读写文件、运行命令、访问网络——但它没有判断力。权限系统就是横亘在”能力”和”安全”之间的那道门。
但 Claude Code 的权限系统不是简单的 allow/deny 布尔值——而是 8 种规则来源、5 种权限模式、12 步分层检查管线。这个复杂度不是过度工程,而是对现实安全需求的诚实回应。
这一章讨论这个设计。
本章路线图
1 | graph LR |
现状:三层决策与五个模式
三种行为:allow、deny、ask
权限系统最基础的抽象是三种行为。打开 src/types/permissions.ts:1
export type PermissionBehavior = 'allow' | 'deny' | 'ask'
allow 意味着放行,deny 意味着拦截,ask 意味着不确定——把决定权交给用户。ask 是这个模型的关键创新:它承认系统无法自动判断所有场景,于是诚实地把模糊地带交还给人类。
在 hasPermissionsToUseTool 函数(src/utils/permissions/permissions.ts)中,决策按固定优先级依次检查:先检查 deny 规则,再检查 ask 规则,最后检查 allow 规则。deny 永远优先——如果一条 deny 规则匹配了,后续的 allow 规则不会覆盖它。
五个权限模式
在三种行为之上,系统定义了五个权限模式:1
2
3
4
5
6
7export const EXTERNAL_PERMISSION_MODES = [
'acceptEdits',
'bypassPermissions',
'default',
'dontAsk',
'plan',
] as const
plan 最保守——AI 只能规划,不能执行。default 是标准模式——读操作自动放行,写操作需确认。acceptEdits 放松一步——文件编辑自动放行。dontAsk 更激进——所有未被显式拒绝的操作自动放行。bypassPermissions 是完全信任。
这五个模式不是凭空设计的。它们对应着用户在实际使用中的不同信任阶段。在 getNextPermissionMode.ts 里,Shift+Tab 切换模式的逻辑——用户可以从保守逐步过渡到信任,或者在任何时候退回安全模式。
8 种规则来源
权限决策来自多个来源,在 src/types/permissions.ts 里定义了 8 种 PermissionRuleSource。规则有不同的持久化目标:session(仅当前会话)、localSettings(本地项目级)、projectSettings(团队共享)、userSettings(全局用户级)。
优先级从高到低。企业管理员配的 enterprise 级配置优先于一切。
Bash 命令的风险分类:2600 行代码的理由
Bash 工具是权限系统最复杂的部分。bashPermissions.ts 超过 2600 行。shell 命令的风险差异极大:ls -la 基本无害,rm -rf / 是灾难性的,curl | bash 是潜在的远程代码执行。
系统实现了多层安全检查:命令拆分(复合命令必须分别检查)、规则匹配(精确匹配、前缀匹配、通配符)、路径约束(项目目录外的操作额外拦截)。匹配之前还要剥离安全包装器——NODE_ENV=test npm run build 在权限检查时应该被等价为 npm run build。但哪些环境变量是”安全”的?系统维护了一个白名单:NODE_ENV、TZ、LANG 等可以剥离,但 PATH、LD_PRELOAD、PYTHONPATH 绝对不能。
Hook 系统:外部安全检查的接口
权限系统不只是内置的规则匹配。Hook 系统允许外部程序参与权限决策:1
2
3
4
5hookEventName: z.literal('PermissionRequest'),
decision: z.union([
z.object({ behavior: z.literal('allow'), ... }),
z.object({ behavior: z.literal('deny'), ... }),
]),
企业可以在权限管道中插入自己的安全策略——合规检查脚本可以在任何工具执行之前做出 allow 或 deny 的决策。
当时还有什么选择
全有或全无
要么完全信任 AI,要么完全限制。这几乎不是设计选择,而是放弃设计。全信任等于没有权限系统,只读模式把 AI 降级为建议引擎。两种极端都忽略了”大部分操作是安全的,少数操作需要审查”这个关键事实。
基于能力的安全(Capability-Based Security)
Android 模型:应用在安装时声明所有权限,用户一次性授权。问题是 AI 的行为在会话开始时无法预知——预先声明所有可能需要的权限会导致过度授权。
纯沙箱执行
Claude Code 有沙箱机制(SandboxManager),但没有把沙箱作为唯一的安全手段。沙箱与便利性存在根本矛盾:在沙箱中运行的构建命令可能因为找不到系统依赖而失败。沙箱应该是工具而不是牢笼。
纯白名单
只允许预先批准的命令和操作。问题是 shell 命令的组合空间几乎是无限的——白名单要么太严格(阻断合法操作),要么太宽松(为了覆盖常见场景而开大洞)。
为什么选了这个
“Ask” 模式是务实的中间道路
ask 不是一个失败状态,而是一个诚实的设计决策。它承认 AI 系统无法完美判断所有操作的风险。与其用一个复杂但可能有漏洞的自动化系统假装无所不知,不如在不确定时停下来,让人类做决定。
渐进式信任构建
信任不是静态的。在一次会话中,你可能从 plan 模式开始,到 default 模式,再到 acceptEdits 模式。Shift+Tab 让你在模式间自由切换。权限缓存在会话内也扮演了信任累积的角色——当你第一次批准 git commit 后,后续的不再询问。
deny 优先于 allow 的安全哲学
即使在 bypassPermissions 模式下,某些安全关键检查(.git/ 路径、shell 配置修改)仍然必须提示。这是 deny-first 的设计:安全关键规则不可覆盖。
如果重新设计
更好的风险分类
当前的 RiskLevel 类型只有三个级别——LOW、MEDIUM、HIGH——而且只在权限解释中使用,没有直接影响决策流程。如果区分操作的可逆性(git commit 可逆、rm 不可逆、sudo 系统级),可能实现更精确的默认策略。
更细粒度的权限
Bash(git:*) 允许所有 git 开头的命令,但 git push --force 和 git status 的风险完全不同。更细的粒度可能更安全,但粒度越细,配置越复杂——经典的 expressiveness vs. usability 权衡。
AI 监督 AI
yoloClassifier.ts 和 bashClassifier.ts 是用另一个 AI 模型来判断当前操作是否安全的尝试。这引入了新的问题:分类器的准确率不是 100%,有延迟和成本,判断可能被恶意输入欺骗。但它减少了用户的负担,把最终的安全责任部分转移到了模型。
试试看
练习一:追踪权限决策
在 hasPermissionsToUseTool 函数的入口和出口加日志,执行一条需要确认的命令(比如 Bash(npm install)),观察决策路径。
练习二:切换权限模式
在 Claude Code 里按 Shift+Tab 切换权限模式,观察不同模式下对同一操作的行为差异。
练习三:自定义权限规则
在 .claude/settings.json 里添加一条 deny 规则,比如拒绝所有 rm 命令。然后尝试让 AI 执行 rm,观察被拒绝的行为。
检查点
- 三种行为:
allow(放行)、deny(拦截)、ask(交给用户) - 五个权限模式:plan -> default -> acceptEdits -> dontAsk -> bypassPermissions
- 8 种规则来源:从 CLI 参数到企业策略的多层配置
- deny 优先于 allow:安全关键规则不可覆盖
- Bash 命令 2600+ 行的安全分析:命令拆分、规则匹配、路径约束
- Hook 系统:外部安全策略可以参与权限决策
- 渐进式信任:Shift+Tab 切换模式,权限缓存累积信任
导航
上一章:第 44 章:工具系统的演进
下一章:第 46 章:有限窗口的智慧