权限系统
五层安全决策
YOLO 自动模式通过转录分类器实现无人值守下的安全执行
学习
概念讲解与核心设计分析
权限系统概述
Claude Code 的权限系统是安全性的最后一道防线。它决定了 Claude 能做什么、不能做什么、需要用户确认后才能做什么。这套系统由 5 层决策逻辑、多种权限模式、命令分类器、沙箱适配器和交互式确认对话框构成。每一次工具调用都要经过这套系统的审查。
5 层权限决策
当一个工具调用到达权限系统时,它按照以下 5 层顺序被评估,第一个做出决策的层"获胜":
第 1 层:全局拒绝规则(Blanket Deny Rules)
最高优先级。这些规则定义了无论如何都不允许的操作:
- 例如:禁止访问
/etc/shadow、禁止执行rm -rf / - 这些规则由系统硬编码,用户无法覆盖
- 如果匹配,立即返回"拒绝",后续层不再评估
第 2 层:始终允许规则(Always-Allow Rules)
用户配置的自动允许规则(在 settings.json 中定义):
- 例如:
"always_allow": ["Read", "Glob", "Grep"]自动允许所有只读工具 - Bash 命令可以使用模式匹配:
"Bash(npm test*)"允许所有以npm test开头的命令 - 如果匹配,立即返回"允许",跳过后续层
第 3 层:始终拒绝规则(Always-Deny Rules)
用户配置的自动拒绝规则:
- 例如:
"always_deny": ["Bash(curl*)", "Bash(wget*)"]禁止网络下载命令 - 优先级低于 always-allow,所以如果同一工具同时在两个列表中,always-allow 获胜
第 4 层:工具自身权限检查(Tool checkPermissions())
每个工具实现自己的 checkPermissions() 方法:
- BashTool 会分析命令内容,判断是否需要用户确认
- FileWriteTool 检查目标路径是否在工作目录内
- 这一层可以返回"允许"、"拒绝"或"需要用户确认"
第 5 层:分类器 / 用户确认(Classifier / User Prompt)
如果前 4 层都没有做出明确决策,系统进入最终判断:
- 在默认模式下,弹出交互式对话框让用户确认
- 在自动模式(YOLO/Auto)下,由分类器自动判断
- 用户确认对话框显示工具名称、输入参数和预期行为
PermissionMode:权限模式
系统支持 4 种权限模式,决定了第 5 层的行为:
- default — 默认模式。未通过前 4 层的调用会弹出用户确认对话框
- plan — 计划模式。只读工具自动允许,写操作工具直接拒绝
- bypassPermissions — 绕过权限。所有工具自动允许(仅用于内部测试)
- auto — 自动模式(YOLO 模式)。由分类器自动判断,不打扰用户
Bash 分类器(bashClassifier.ts)
BashTool 的权限判断比其他工具更复杂,因为 shell 命令的风险差异极大。bashClassifier.ts 专门负责 Bash 命令的安全分类:
- 结合命令的 AST 分析结果(来自 Tree-sitter)
- 判断命令是否涉及文件修改、网络访问、进程管理等高风险操作
- 为每个命令生成安全评分和风险标签
- 根据评分决定是否需要用户确认
YOLO/Auto 模式分类器(yoloClassifier.ts)
在自动模式下,yoloClassifier.ts 负责在不打扰用户的情况下做安全判断:
- 分析整个对话上下文(transcript-level),而不只是单个工具调用
- 考虑 Claude 为什么要执行这个操作(基于对话历史)
- 对明显安全的操作(如用户刚要求的文件编辑)自动放行
- 对可疑操作(如未预期的
rm命令)仍然拒绝
拒绝追踪与回退提示(Denial Tracking)
当分类器多次拒绝 Claude 的工具调用时,系统会触发回退逻辑:
shouldFallbackToPrompting()检测重复拒绝的模式- 如果同类操作被连续拒绝 3 次以上,回退到用户提示模式
- 这避免了分类器过于保守导致 Claude 无法完成任务
沙箱适配器(sandbox-adapter.ts)
对于需要在隔离环境中执行的操作,sandbox-adapter.ts 提供了统一的沙箱接口:
- 抽象不同平台的沙箱实现(macOS sandbox-exec、Linux namespaces 等)
- 限制文件系统访问范围(只能访问工作目录和显式挂载的路径)
- 限制网络访问
- 限制进程能力(不允许提权操作)
useCanUseTool React Hook
useCanUseTool 是前端侧的权限集成点,连接后端权限系统和 UI 交互:
- 当工具调用需要用户确认时,该 hook 触发一个交互式对话框
- 对话框显示:工具名称、命令内容、预期行为、风险提示
- 用户可以选择"允许"、"拒绝"、"始终允许此类操作"
- 支持 coordinator/swarm 模式下的多 Agent 权限管理
Shell 规则匹配
Bash 命令的权限规则支持模式匹配(Pattern Matching):
Bash(git *)— 匹配所有以git开头的命令Bash(npm run *)— 匹配所有npm run命令Bash(cd *)— 匹配所有目录切换命令- 模式使用 glob 语法,
*匹配任意字符序列 - 匹配在 AST 解析后进行,确保不会被引号嵌套等语法技巧绕过
PermissionDecisionReason
每个权限决策都附带原因说明,用于审计和调试:
classifier— 由分类器自动判断hook— 由钩子拦截rule— 由配置规则匹配subcommandResults— 基于子命令分析结果mode— 由当前权限模式决定sandboxOverride— 沙箱覆盖workingDir— 工作目录检查safetyCheck— 安全检查
架构
模块关系与设计决策
权限系统架构
核心模块
| 模块 | 职责 | 关键函数/类型 |
|---|---|---|
| utils/permissions/ | 权限决策引擎 | hasPermissionsToUseTool() |
| bashClassifier.ts | Bash 命令安全分类 | classifyBashCommand() |
| yoloClassifier.ts | 自动模式安全分类 | classifyTranscript() |
| sandbox-adapter.ts | 沙箱接口适配 | createSandbox(), runInSandbox() |
| useCanUseTool | UI 权限对话框 | React Hook + Dialog |
5 层权限决策流程
- Layer 1: Blanket Deny → 系统硬编码的绝对禁止规则
- 匹配 → DENY(不可覆盖)
- Layer 2: Always-Allow → 用户配置的自动允许
- 匹配 → ALLOW
- Layer 3: Always-Deny → 用户配置的自动拒绝
- 匹配 → DENY
- Layer 4: Tool checkPermissions() → 工具自身的检查逻辑
- allow/deny/ask → 传递对应决策
- Layer 5: Classifier / User Prompt → 最终裁定
- default 模式 → 弹窗让用户确认
- auto 模式 → yoloClassifier 自动判断
- plan 模式 → 写操作直接拒绝
PermissionMode 行为矩阵
| 模式 | 只读工具 | 写操作工具 | 未知风险工具 |
|---|---|---|---|
| default | 自动允许 | 用户确认 | 用户确认 |
| plan | 自动允许 | 直接拒绝 | 直接拒绝 |
| auto | 自动允许 | 分类器判断 | 分类器判断 |
| bypassPermissions | 自动允许 | 自动允许 | 自动允许 |
拒绝追踪与回退
- 追踪:记录每次分类器拒绝的工具名和原因
- 检测:shouldFallbackToPrompting() 检查连续拒绝次数
- 回退:超过阈值(3次),切换到用户确认模式
- 重置:用户做出决策后,重置计数器
设计决策
为什么权限系统是 5 层而不是更简单的 allow/deny 二元决策?
单一层级无法满足不同场景的需求:系统级安全需要硬编码拒绝(第 1 层),用户偏好需要可配置规则(第 2/3 层),工具特有逻辑需要自定义检查(第 4 层),兜底策略需要分类器或人工确认(第 5 层)。每一层解决一个独立的关注点,组合在一起形成了既安全又灵活的权限体系。
源码
共 3 个关键代码示例
// utils/permissions/hasPermissionsToUseTool.ts — 简化版
export type PermissionDecision = {
allowed: boolean
reason: PermissionDecisionReason
message?: string
}
export type PermissionDecisionReason =
| 'classifier'
| 'hook'
| 'rule'
| 'subcommandResults'
| 'mode'
| 'sandboxOverride'
| 'workingDir'
| 'safetyCheck'
export async function hasPermissionsToUseTool(
tool: Tool,
input: unknown,
context: ToolPermissionContext
): Promise<PermissionDecision> {
// ===== 第 1 层:全局拒绝规则 =====
const blanketDeny = checkBlanketDenyRules(tool, input)
if (blanketDeny) {
return {
allowed: false,
reason: 'safetyCheck',
message: blanketDeny.message,
}
}
// ===== 第 2 层:始终允许规则 =====
if (context.alwaysAllowRules.length > 0) {
const allowMatch = matchPermissionRule(
tool, input, context.alwaysAllowRules
)
if (allowMatch) {
return { allowed: true, reason: 'rule' }
}
}
// ===== 第 3 层:始终拒绝规则 =====
if (context.alwaysDenyRules.length > 0) {
const denyMatch = matchPermissionRule(
tool, input, context.alwaysDenyRules
)
if (denyMatch) {
return {
allowed: false,
reason: 'rule',
message: '被拒绝规则阻止: ' + denyMatch.pattern,
}
}
}
// ===== 第 4 层:工具自身权限检查 =====
const toolPermResult = await tool.checkPermissions(input, context)
if (toolPermResult.decided) {
return {
allowed: toolPermResult.allowed,
reason: toolPermResult.reason,
message: toolPermResult.message,
}
}
// ===== 第 5 层:根据模式决定 =====
switch (context.mode) {
case 'plan':
// 计划模式:只读工具允许,其余拒绝
return tool.isReadOnly()
? { allowed: true, reason: 'mode' }
: { allowed: false, reason: 'mode', message: '计划模式下不允许写操作' }
case 'bypassPermissions':
return { allowed: true, reason: 'mode' }
case 'auto':
// 自动模式:使用分类器
return classifyForAutoMode(tool, input, context)
case 'default':
default:
// 默认模式:需要用户确认
return {
allowed: false,
reason: 'classifier',
message: 'needsUserConfirmation',
}
}
}
function checkBlanketDenyRules(
tool: Tool,
input: unknown
): { message: string } | null {
// 硬编码的绝对禁止规则
if (tool.name === 'Bash') {
const cmd = (input as { command: string }).command
if (cmd.includes('rm -rf /') && !cmd.includes('rm -rf /tmp')) {
return { message: '禁止执行可能删除根目录的命令' }
}
}
return null
}// bashClassifier.ts — Bash 命令安全分类
export interface BashClassification {
riskLevel: 'safe' | 'moderate' | 'dangerous'
category: string
requiresConfirmation: boolean
explanation: string
}
export function classifyBashCommand(
command: string,
astAnalysis: CommandAnalysis
): BashClassification {
// 已知安全命令
if (astAnalysis.isReadOnly && !astAnalysis.hasRedirection) {
return {
riskLevel: 'safe',
category: 'read-only',
requiresConfirmation: false,
explanation: '只读命令,无副作用',
}
}
// 已知危险命令模式
const dangerousPatterns = [
{ pattern: /rms+(-rf?|--recursive)/, category: 'destructive-delete' },
{ pattern: /chmods+777/, category: 'insecure-permissions' },
{ pattern: />s*/etc//, category: 'system-file-write' },
{ pattern: /curl.*|s*(bash|sh)/, category: 'remote-code-execution' },
{ pattern: /gits+pushs+.*--force/, category: 'force-push' },
]
for (const { pattern, category } of dangerousPatterns) {
if (pattern.test(command)) {
return {
riskLevel: 'dangerous',
category,
requiresConfirmation: true,
explanation: '检测到高风险操作: ' + category,
}
}
}
// 有写操作但不是已知危险的
if (!astAnalysis.isReadOnly || astAnalysis.hasRedirection) {
return {
riskLevel: 'moderate',
category: 'write-operation',
requiresConfirmation: true,
explanation: '命令可能修改文件系统',
}
}
return {
riskLevel: 'safe',
category: 'unknown-safe',
requiresConfirmation: false,
explanation: '未检测到风险',
}
}
// yoloClassifier.ts — 自动模式分类
export async function classifyForAutoMode(
tool: Tool,
input: unknown,
context: ToolPermissionContext
): Promise<PermissionDecision> {
// 只读工具:始终允许
if (tool.isReadOnly()) {
return { allowed: true, reason: 'classifier' }
}
// Bash 命令:使用专用分类器
if (tool.name === 'Bash') {
const cmd = (input as { command: string }).command
const analysis = analyzeCommand(cmd)
const classification = classifyBashCommand(cmd, analysis)
if (classification.riskLevel === 'dangerous') {
return {
allowed: false,
reason: 'classifier',
message: classification.explanation,
}
}
// 中等风险:检查拒绝追踪
if (classification.riskLevel === 'moderate') {
if (shouldFallbackToPrompting(tool.name, context)) {
return {
allowed: false,
reason: 'classifier',
message: 'needsUserConfirmation',
}
}
return { allowed: true, reason: 'classifier' }
}
}
// 文件写操作:检查是否在工作目录内
if (tool.name === 'Edit' || tool.name === 'Write') {
const filePath = (input as { file_path: string }).file_path
if (isWithinWorkingDir(filePath, context)) {
return { allowed: true, reason: 'workingDir' }
}
return {
allowed: false,
reason: 'workingDir',
message: '文件不在工作目录内',
}
}
// 默认:允许
return { allowed: true, reason: 'classifier' }
}// 权限规则匹配系统
interface PermissionRule {
pattern: string // "Bash(git *)", "Read", "Edit"
}
export function matchPermissionRule(
tool: Tool,
input: unknown,
rules: PermissionRule[]
): PermissionRule | null {
for (const rule of rules) {
if (matchSingleRule(tool, input, rule)) {
return rule
}
}
return null
}
function matchSingleRule(
tool: Tool,
input: unknown,
rule: PermissionRule
): boolean {
// 简单工具名匹配: "Read", "Glob"
if (!rule.pattern.includes('(')) {
return tool.name === rule.pattern
}
// 带参数的匹配: "Bash(npm test*)"
const match = rule.pattern.match(/^(\w+)\((.+)\)$/)
if (!match) return false
const [, toolName, commandPattern] = match
if (tool.name !== toolName) return false
// 对 Bash 工具,匹配命令内容
if (toolName === 'Bash') {
const command = (input as { command: string }).command
return globMatch(command.trim(), commandPattern)
}
// 对文件工具,匹配文件路径
if (toolName === 'Read' || toolName === 'Edit' || toolName === 'Write') {
const filePath = (input as { file_path: string }).file_path
return globMatch(filePath, commandPattern)
}
return false
}
// glob 匹配:* 匹配任意字符序列
function globMatch(text: string, pattern: string): boolean {
// 转义正则特殊字符,然后将 glob 通配符转换为正则
const escaped = escapeRegExp(pattern)
const regexStr = '^' + escaped
.replace(/\\\*/g, '.*') // glob * → regex .*
.replace(/\\\?/g, '.') // glob ? → regex .
+ '$'
return new RegExp(regexStr).test(text)
}
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^$\\|()\[\]]/g, '\\$&')
}
// 拒绝追踪与回退
const denialCounts = new Map<string, number>()
export function shouldFallbackToPrompting(
toolName: string,
context: ToolPermissionContext
): boolean {
const key = toolName
const count = denialCounts.get(key) ?? 0
return count >= 3 // 连续拒绝 3 次后回退到用户确认
}
export function trackDenial(toolName: string): void {
const count = denialCounts.get(toolName) ?? 0
denialCounts.set(toolName, count + 1)
}
export function resetDenials(toolName: string): void {
denialCounts.delete(toolName)
}
// useCanUseTool — React Hook (简化版)
export function useCanUseTool(toolCall: ToolUseBlock) {
const [decision, setDecision] = useState<PermissionDecision | null>(null)
const [showDialog, setShowDialog] = useState(false)
useEffect(() => {
async function check() {
const result = await hasPermissionsToUseTool(
toolCall.tool, toolCall.input, getPermissionContext()
)
if (result.message === 'needsUserConfirmation') {
setShowDialog(true) // 弹出确认对话框
} else {
setDecision(result)
}
}
check()
}, [toolCall])
const handleUserDecision = (
allowed: boolean,
alwaysAllow: boolean
) => {
setDecision({ allowed, reason: 'classifier' })
if (alwaysAllow) {
// 将规则添加到 always-allow 列表
addAlwaysAllowRule(toolCall.tool.name, toolCall.input)
}
setShowDialog(false)
}
return { decision, showDialog, handleUserDecision }
}互动
步进式流程演示
权限系统互动解析
第 1 步:理解 5 层决策的必要性
让我们用一个具体例子走过 5 层决策:Claude 调用 Bash("git push --force origin main")
- 第 1 层(全局拒绝):虽然危险,但不在硬编码禁止列表中 → 通过
- 第 2 层(始终允许):用户配置了
Bash(git *)→ 匹配!但等等...这真的安全吗?
这就是 5 层设计的问题:如果用户配置了过于宽泛的 always-allow 规则,git push --force 会被自动允许。Claude Code 的文档强调:不要使用过于宽泛的规则。正确的做法是 Bash(git status*)、Bash(git log*) 这样的精确规则。
第 2 步:Bash 分类器的深度分析
Bash 分类器不是简单的黑名单匹配,而是理解命令语义:
cat file.txt→ safe(只读)cat file.txt > output.txt→ moderate(有重定向,可能覆盖文件)cat file.txt | grep pattern→ safe(管道不产生副作用)$(cat file.txt)→ moderate(命令替换,结果可能被执行)
关键洞察:同一个 cat 命令在不同上下文中有不同的风险等级。这就是为什么需要 AST 分析而不是简单的命令名匹配。
第 3 步:YOLO 模式的安全边界
Auto/YOLO 模式并不是"允许一切"。它的核心原则是:
- 自动允许:与用户明确要求一致的操作("帮我编辑 config.ts" → 允许编辑 config.ts)
- 自动拒绝:明显危险的操作(rm -rf、curl | bash)
- 回退确认:不确定的操作连续被拒绝 3 次后,切换到人工确认
这意味着即使在 YOLO 模式下,用户仍然有安全保护。分类器的目标是"像一个有经验的开发者那样判断"。
第 4 步:沙箱的隔离机制
沙箱为不信任的命令提供了一个隔离的执行环境:
- 文件系统隔离:命令只能看到工作目录及其子目录,无法访问 ~/.ssh、/etc 等
- 网络隔离:可选地禁止网络访问
- 进程隔离:命令无法提升权限或影响其他进程
- 使用场景:执行不受信任的构建脚本、运行测试等
第 5 步:用户确认对话框的设计
权限确认对话框提供了三个选项:
- "允许" — 本次允许,下次同类操作仍需确认
- "拒绝" — 本次拒绝,Claude 收到错误信息并调整策略
- "始终允许" — 将此操作模式添加到 always-allow 规则,后续自动放行
这种渐进式信任模型让用户可以:
- 初始保持谨慎(每次都确认)
- 逐渐建立信任(对常用操作设置"始终允许")
- 最终达到高效状态(大多数操作自动放行,只有异常操作需要确认)
关键设计洞察
- 纵深防御:5 层决策确保即使某一层被绕过,后续层仍能拦截
- 可配置性:用户可以通过规则精确控制权限边界
- 渐进信任:从"全部确认"到"大部分自动"的平滑过渡
- 安全分类:Bash 分类器理解命令语义,而非简单的模式匹配
- 回退机制:分类器不确定时回退到人工确认,避免过度阻塞