第 16 篇:权限系统 — AI 安全的最后一道防线
本篇是《深入 Claude Code 源码》系列第 16 篇。我们将深入权限系统的完整架构:从权限模式、规则体系、决策管线到 AI Classifier 辅助判断,揭示一个生产级 AI Agent 如何在”自动化”与”安全”之间找到平衡。
为什么权限系统是 AI 产品中最关键的模块?
传统 CLI 工具的权限模型很简单——用户敲什么命令就执行什么命令,责任完全在用户。但 AI Agent 改变了这个范式:模型自主决定执行什么操作。用户说”帮我重构这个项目”,模型可能决定删除文件、执行 shell 命令、修改配置——这些操作的风险程度天差地别。
这意味着,AI 产品的权限系统面临独特的挑战:
- 操作不可预测:用户无法提前知道模型会调用哪些工具、传入什么参数
- 风险谱系宽广:从读取文件(无害)到
rm -rf /(灾难性),所有操作都通过同一个工具接口
- 效率与安全的矛盾:每次都弹确认对话框会让用户抓狂,但完全自动化又有安全风险
Claude Code 的权限系统用 多层防线 + 渐进式信任 的方式解决了这个矛盾。本篇将从三个层面解析:
- 权限模式(Permission Mode):用户的全局信任级别
- 规则系统(Permission Rules):细粒度的 allow/deny/ask 规则
- 决策管线(Permission Pipeline):
hasPermissionsToUseTool() 的完整判定流程
一、权限模式:用户的全局信任级别
1.1 七种权限模式
Claude Code 定义了七种权限模式,代表用户对 AI 操作的不同信任程度:
1 2 3 4 5 6 7 8 9 10 11 12
| export const EXTERNAL_PERMISSION_MODES = [ 'acceptEdits', 'bypassPermissions', 'default', 'dontAsk', 'plan', ] as const
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble' export type PermissionMode = InternalPermissionMode
|
每种模式的语义如下:
| 模式 |
信任级别 |
行为 |
default |
最低 |
每个非只读操作都需要用户确认 |
plan |
只读 |
只能读取和搜索,写入操作需要确认 |
acceptEdits |
中等 |
工作目录内的文件编辑自动允许,其他操作仍需确认 |
bypassPermissions |
高 |
跳过大部分权限检查(但 safety check 仍然生效) |
dontAsk |
特殊 |
不弹出确认对话框,需要确认的操作自动拒绝 |
auto |
内部 |
用 AI Classifier 自动判断操作安全性(仅限 Anthropic 内部) |
bubble |
内部 |
类型系统中定义但不在用户可达的运行时模式集合中 |
注意 auto 模式是通过 feature('TRANSCRIPT_CLASSIFIER') 编译期门控的——外部构建中这个模式的代码会被 DCE 完全移除:
1 2 3 4 5
| export const INTERNAL_PERMISSION_MODES = [ ...EXTERNAL_PERMISSION_MODES, ...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)), ] as const satisfies readonly PermissionMode[]
|
1.2 模式切换:Shift+Tab 循环
用户可以通过 Shift+Tab 在模式之间循环切换。切换顺序不是简单的线性,而是根据用户类型和可用性动态决定的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| export function getNextPermissionMode( toolPermissionContext: ToolPermissionContext, ): PermissionMode { switch (toolPermissionContext.mode) { case 'default': if (process.env.USER_TYPE === 'ant') { if (toolPermissionContext.isBypassPermissionsModeAvailable) { return 'bypassPermissions' } if (canCycleToAuto(toolPermissionContext)) { return 'auto' } return 'default' } return 'acceptEdits'
case 'acceptEdits': return 'plan'
case 'plan': if (toolPermissionContext.isBypassPermissionsModeAvailable) { return 'bypassPermissions' } return 'default'
} }
|
外部用户的典型切换路径是:default → acceptEdits → plan → default(或加上 bypassPermissions)。内部用户则简化为 default → bypassPermissions → auto → default。
二、规则系统:细粒度的 allow/deny/ask
权限模式是”粗粒度”的全局控制,而规则系统提供了针对具体工具和操作的细粒度控制。
2.1 规则的数据结构
每条权限规则由三个部分组成:
1 2 3 4 5 6 7 8 9 10 11 12
| export type PermissionRule = { source: PermissionRuleSource ruleBehavior: PermissionBehavior ruleValue: PermissionRuleValue }
export type PermissionRuleValue = { toolName: string ruleContent?: string }
|
规则的来源(source)有 8 种。需要注意的是,PERMISSION_RULE_SOURCES 定义的顺序是搜索/遍历顺序,而不是严格的”高优先级覆盖低优先级”的语义——getAllowRules() / getDenyRules() 等函数遍历所有来源后 flatMap 成一个数组,查找时返回第一个匹配的规则:
1 2 3 4 5 6 7 8
| const PERMISSION_RULE_SOURCES = [ ...SETTING_SOURCES, 'cliArg', 'command', 'session', ] as const satisfies readonly PermissionRuleSource[]
|
其中 SETTING_SOURCES 本身的合并语义是”后覆盖前”(utils/settings/constants.ts:6 注释明确写了 “later sources override earlier ones”),即 policySettings > flagSettings > localSettings > projectSettings > userSettings。但权限规则的实际匹配是所有来源的规则 flatMap 后按序扫描,第一个匹配就返回——这与 settings 的覆盖语义有微妙区别。
2.2 规则的存储格式
规则存储在 settings.json 文件中,格式是 ToolName(ruleContent):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { "permissions": { "allow": [ "Bash(npm install:*)", "Bash(git status)", "FileEdit", "mcp__server1" ], "deny": [ "Bash(rm -rf:*)", "Bash(curl:*)" ], "ask": [ "Bash(npm publish:*)" ] } }
|
规则解析由 permissionRuleValueFromString() 处理,支持转义括号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export function permissionRuleValueFromString( ruleString: string, ): PermissionRuleValue { const openParenIndex = findFirstUnescapedChar(ruleString, '(') if (openParenIndex === -1) { return { toolName: normalizeLegacyToolName(ruleString) } } const toolName = ruleString.substring(0, openParenIndex) const rawContent = ruleString.substring(openParenIndex + 1, closeParenIndex) const ruleContent = unescapeRuleContent(rawContent) return { toolName: normalizeLegacyToolName(toolName), ruleContent } }
|
注意 normalizeLegacyToolName() 会将旧工具名映射到新名——比如 Task → Agent,KillShell → TaskStop,确保旧配置不会失效。还有一个容易忽略的细节:Bash() 和 Bash(*) 在解析时都会被归约为 { toolName: 'Bash' }(没有 ruleContent),即”整工具级别”的规则,与 Bash 等价(permissionRuleParser.ts:124-127)。
2.3 多源规则加载与企业管控
规则从多个配置源加载,但企业管理员可以通过 allowManagedPermissionRulesOnly 限制磁盘加载阶段仅使用受管规则。不过需要注意,这个限制作用于 loadAllPermissionRulesFromDisk()——初始化时通过 CLI --allowedTools / --disallowedTools 传入的规则仍然会被写入 context 的 cliArg source,后续由 syncPermissionRulesFromDisk() 在设置变更时清理非 policy 来源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export function loadAllPermissionRulesFromDisk(): PermissionRule[] { if (shouldAllowManagedPermissionRulesOnly()) { return getPermissionRulesForSource('policySettings') }
const rules: PermissionRule[] = [] for (const source of getEnabledSettingSources()) { rules.push(...getPermissionRulesForSource(source)) } return rules }
|
2.4 Shell 命令的三种匹配模式
Bash 工具的权限规则支持三种匹配方式,由 parsePermissionRule() 解析:
1 2 3 4 5
| export type ShellPermissionRule = | { type: 'exact'; command: string } | { type: 'prefix'; prefix: string } | { type: 'wildcard'; pattern: string }
|
通配符匹配通过 matchWildcardPattern() 实现,它将 * 转换为正则表达式的 .*,支持 \* 转义字面星号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export function matchWildcardPattern( pattern: string, command: string, caseInsensitive = false, ): boolean { const unescapedStarCount = (processed.match(/\*/g) || []).length if (regexPattern.endsWith(' .*') && unescapedStarCount === 1) { regexPattern = regexPattern.slice(0, -3) + '( .*)?' } const regex = new RegExp(`^${regexPattern}$`, flags) return regex.test(command) }
|
这是权限系统的核心——每次模型请求使用工具时,都会经过 hasPermissionsToUseTool() 函数的完整判定管线。这个管线分为内层决策(hasPermissionsToUseToolInner)和外层包装两部分。
3.1 决策结果类型
管线返回三种可能的决策:
1 2 3 4 5
| export type PermissionDecision = | PermissionAllowDecision | PermissionAskDecision | PermissionDenyDecision
|
此外,工具内部的 checkPermissions() 还可以返回 passthrough——表示”我没有意见,交给通用权限系统决定”:
1 2 3 4 5 6 7 8
| export type PermissionResult = | PermissionDecision | { behavior: 'passthrough' message: string }
|
3.2 内层决策管线(7 步)
hasPermissionsToUseToolInner() 是权限检查的核心,按照严格的优先级顺序执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| flowchart TD START["工具请求到达"] --> S1A{"1a. 整工具<br/>被 deny?"} S1A -->|是| DENY1["❌ 拒绝<br/>规则来源"] S1A -->|否| S1B{"1b. 整工具<br/>被 ask?"} S1B -->|是<br/>且无沙箱豁免| ASK1["⏸ 需确认"] S1B -->|否或可沙箱豁免| S1C["1c. 调用<br/>tool.checkPermissions()"] S1C --> S1D{"1d. 工具实现<br/>返回 deny?"} S1D -->|是| DENY2["❌ 拒绝<br/>工具实现"] S1D -->|否| S1E{"1e. 需要用户<br/>交互?"} S1E -->|是且 ask| ASK2["⏸ 需确认<br/>如 AskUserQuestion"] S1E -->|否| S1F{"1f. 内容级<br/>ask 规则?"} S1F -->|是| ASK3["⏸ 需确认<br/>如 Bash(npm publish:*)"] S1F -->|否| S1G{"1g. Safety<br/>Check 触发?"} S1G -->|是| ASK4["⏸ 需确认<br/>如 .git/ .claude/ 文件"] S1G -->|否| S2A{"2a. bypass<br/>模式?"} S2A -->|是| ALLOW1["✅ 允许<br/>模式豁免"] S2A -->|否| S2B{"2b. 整工具被<br/>allow 规则覆盖?"} S2B -->|是| ALLOW2["✅ 允许<br/>规则来源"] S2B -->|否| S3["3. passthrough<br/>→ ask"] S3 --> ASK5["⏸ 需确认"]
style DENY1 fill:#f44336,color:#fff style DENY2 fill:#f44336,color:#fff style ALLOW1 fill:#4caf50,color:#fff style ALLOW2 fill:#4caf50,color:#fff style ASK1 fill:#ff9800,color:#fff style ASK2 fill:#ff9800,color:#fff style ASK3 fill:#ff9800,color:#fff style ASK4 fill:#ff9800,color:#fff style ASK5 fill:#ff9800,color:#fff
|
对应的代码(permissions.ts:1158-1319):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| async function hasPermissionsToUseToolInner( tool, input, context, ): Promise<PermissionDecision> { let appState = context.getAppState()
const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool) if (denyRule) { return { behavior: 'deny', } }
const askRule = getAskRuleForTool(appState.toolPermissionContext, tool) if (askRule) { const canSandboxAutoAllow = tool.name === BASH_TOOL_NAME && SandboxManager.isSandboxingEnabled() && SandboxManager.isAutoAllowBashIfSandboxedEnabled() && shouldUseSandbox(input) if (!canSandboxAutoAllow) { return { behavior: 'ask', } } }
let toolPermissionResult: PermissionResult = { behavior: 'passthrough', } try { const parsedInput = tool.inputSchema.parse(input) toolPermissionResult = await tool.checkPermissions(parsedInput, context) } catch (e) { }
if (toolPermissionResult?.behavior === 'deny') return toolPermissionResult
if (tool.requiresUserInteraction?.() && toolPermissionResult?.behavior === 'ask') { return toolPermissionResult }
if (toolPermissionResult?.behavior === 'ask' && toolPermissionResult.decisionReason?.type === 'rule' && toolPermissionResult.decisionReason.rule.ruleBehavior === 'ask') { return toolPermissionResult }
if (toolPermissionResult?.behavior === 'ask' && toolPermissionResult.decisionReason?.type === 'safetyCheck') { return toolPermissionResult }
appState = context.getAppState() const shouldBypassPermissions = appState.toolPermissionContext.mode === 'bypassPermissions' || (appState.toolPermissionContext.mode === 'plan' && appState.toolPermissionContext.isBypassPermissionsModeAvailable) if (shouldBypassPermissions) { return { behavior: 'allow', } }
const alwaysAllowedRule = toolAlwaysAllowedRule(appState.toolPermissionContext, tool) if (alwaysAllowedRule) { return { behavior: 'allow', } }
return toolPermissionResult.behavior === 'passthrough' ? { ...toolPermissionResult, behavior: 'ask' } : toolPermissionResult }
|
关键设计决策:注意步骤 1f 和 1g 在步骤 2a(bypass 检查)之前。这意味着:
- 用户显式配置的
ask 规则,即使在 bypass 模式下也会触发确认
- Safety check(保护
.git/、.claude/ 等危险路径),在 bypass 模式下也不会被跳过
但 safety check 并非铁墙一块——在 auto 模式中,safety check 结果会根据其 classifierApprovable 属性区分处理(permissions.ts:526-548):标记为 classifierApprovable: true 的 safety check(如 .claude/、.git/ 下的敏感文件路径)会继续进入 Classifier 流程,由 Classifier 看到完整上下文后判断是否安全放行;而 classifierApprovable: false 的(如 Windows 路径绕过尝试)则即使在 auto 模式下也必须走人工确认。
此外,文件写入工具的 checkWritePermissionForTool() 中,session 级别的 .claude/** allow 规则可以在 safety check 之前生效(filesystem.ts:1252-1300),允许用户在当前会话中临时授权 Claude 编辑自身配置——但该豁免被严格限定为 session source、必须匹配 .claude/ 前缀、且禁止 .. 路径穿越。
这是一个”防御纵深”的设计——bypass 模式跳过的是通用的”未配置规则时的默认 ask”,而不是用户刻意设置的安全屏障。但”纵深”不意味着”绝对不可穿透”,而是在每层设置了精确的豁免条件。
3.3 外层包装:模式级变换
hasPermissionsToUseTool() 是外层包装,对内层返回的 ask 决策进行模式级变换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| export const hasPermissionsToUseTool = async (tool, input, context, ...) => { const result = await hasPermissionsToUseToolInner(tool, input, context)
if (result.behavior === 'allow') { return result }
if (result.behavior === 'ask') { const mode = appState.toolPermissionContext.mode
if (mode === 'dontAsk') { return { behavior: 'deny', message: DONT_ASK_REJECT_MESSAGE(tool.name) } }
if (mode === 'auto') { }
if (appState.toolPermissionContext.shouldAvoidPermissionPrompts) { const hookDecision = await runPermissionRequestHooksForHeadlessAgent(...) if (hookDecision) return hookDecision return { behavior: 'deny', message: AUTO_REJECT_MESSAGE(tool.name) } } }
return result }
|
四、Auto Mode:AI Classifier 辅助安全判断
Auto 模式是权限系统中最有技术含量的部分——它用一个独立的 AI 模型来判断主模型请求的操作是否安全。
4.1 三层快速通道(避免不必要的 Classifier 调用)
Classifier API 调用是昂贵的,因此在调用它之前,系统会先检查三个快速通道:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
if (tool.name !== AGENT_TOOL_NAME && tool.name !== REPL_TOOL_NAME) { const acceptEditsResult = await tool.checkPermissions(parsedInput, { ...context, getAppState: () => ({ ...state, toolPermissionContext: { ...state.toolPermissionContext, mode: 'acceptEdits' }, }), }) if (acceptEditsResult.behavior === 'allow') { return { behavior: 'allow', decisionReason: { type: 'mode', mode: 'auto' } } } }
if (classifierDecisionModule.isAutoModeAllowlistedTool(tool.name)) { return { behavior: 'allow', } }
const classifierResult = await classifyYoloAction(context.messages, action, ...)
|
安全工具白名单定义在 classifierDecision.ts 中:
1 2 3 4 5 6 7 8 9 10 11
| const SAFE_YOLO_ALLOWLISTED_TOOLS = new Set([ FILE_READ_TOOL_NAME, GREP_TOOL_NAME, GLOB_TOOL_NAME, TODO_WRITE_TOOL_NAME, TASK_CREATE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, ])
|
4.2 Denial Tracking:连续拒绝熔断
当 Classifier 连续多次拒绝操作时,系统的处理方式取决于运行环境(permissions.ts:984-1058):
- 交互式 CLI:回退到让用户手动审批(弹出确认对话框),而不是无限循环拒绝
- Headless 模式(后台 Agent、远程调用):直接抛出
AbortError 中止整个 Agent——因为没有用户可以审批,继续重试只是浪费 token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export type DenialTrackingState = { consecutiveDenials: number totalDenials: number }
export const DENIAL_LIMITS = { maxConsecutive: 3, maxTotal: 20, } as const
export function shouldFallbackToPrompting(state: DenialTrackingState): boolean { return ( state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive || state.totalDenials >= DENIAL_LIMITS.maxTotal ) }
|
每次 Classifier 允许操作时,连续拒绝计数会被重置:
1 2 3 4
| export function recordSuccess(state: DenialTrackingState): DenialTrackingState { if (state.consecutiveDenials === 0) return state return { ...state, consecutiveDenials: 0 } }
|
4.3 Classifier 不可用时的 Fail-Closed 策略
当 Classifier API 调用失败时,系统通过 GrowthBook Feature Flag 控制是”fail closed”(拒绝)还是”fail open”(回退到用户确认):
1 2 3 4 5 6 7 8 9 10 11
| if (classifierResult.unavailable) { if (getFeatureValue_CACHED_WITH_REFRESH( 'tengu_iron_gate_closed', true, 30 * 60 * 1000, )) { return { behavior: 'deny', message: buildClassifierUnavailableMessage(...) } } return result }
|
五、文件系统路径验证:多维安全检查
文件操作是 AI Agent 最常见的操作类型,路径验证是权限系统中最复杂的部分之一。
5.1 isPathAllowed() 的五步验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| export function isPathAllowed( resolvedPath, context, operationType, precomputedPathsToCheck?, ): PathCheckResult { const denyRule = matchingRuleForInput(resolvedPath, context, permissionType, 'deny') if (denyRule) return { allowed: false, decisionReason: { type: 'rule', rule: denyRule } }
if (operationType !== 'read') { const internalEditResult = checkEditableInternalPath(resolvedPath, {}) if (internalEditResult.behavior === 'allow') return { allowed: true, } }
if (operationType !== 'read') { const safetyCheck = checkPathSafetyForAutoEdit(resolvedPath, ...) if (!safetyCheck.safe) return { allowed: false, } }
if (pathInAllowedWorkingPath(resolvedPath, context, ...)) { if (operationType === 'read' || context.mode === 'acceptEdits') { return { allowed: true } } }
if (operationType !== 'read' && !isInWorkingDir && isPathInSandboxWriteAllowlist(resolvedPath)) { return { allowed: true, } }
const allowRule = matchingRuleForInput(resolvedPath, context, permissionType, 'allow') if (allowRule) return { allowed: true, }
return { allowed: false } }
|
5.2 路径安全验证:防止 TOCTOU 攻击
validatePath() 在路径验证前会做多项安全检查,防止 shell 展开与验证之间的 TOCTOU 漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| export function validatePath(path, cwd, toolPermissionContext, operationType) { const cleanPath = expandTilde(path.replace(/^['"]|['"]$/g, ''))
if (containsVulnerableUncPath(cleanPath)) { return { allowed: false, reason: 'UNC network paths require manual approval' } }
if (cleanPath.startsWith('~')) { return { allowed: false, reason: 'Tilde expansion variants require manual approval' } }
if (cleanPath.includes('$') || cleanPath.includes('%') || cleanPath.startsWith('=')) { return { allowed: false, reason: 'Shell expansion syntax requires manual approval' } }
if (GLOB_PATTERN_REGEX.test(cleanPath) && (operationType === 'write' || 'create')) { return { allowed: false, reason: 'Glob patterns are not allowed in write operations' } } }
|
5.3 危险删除路径检测
isDangerousRemovalPath() 防止删除根目录、家目录、系统目录等灾难性路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export function isDangerousRemovalPath(resolvedPath: string): boolean { if (forwardSlashed === '*' || forwardSlashed.endsWith('/*')) return true if (normalizedPath === '/') return true if (WINDOWS_DRIVE_ROOT_REGEX.test(normalizedPath)) return true if (normalizedPath === normalizedHome) return true if (dirname(normalizedPath) === '/') return true if (WINDOWS_DRIVE_CHILD_REGEX.test(normalizedPath)) return true return false }
|
六、危险权限检测:Auto Mode 入口的安全门卫
当用户切换到 Auto 模式时,系统会自动剥离危险的 allow 规则(stripDangerousPermissionsForAutoMode(),permissionSetup.ts:510-555)。这是因为 Auto 模式依赖 Classifier 来判断安全性——如果存在绕过 Classifier 的 allow 规则,模型就能不经审查地执行危险操作。
危险权限检测覆盖三类工具(isDangerousClassifierPermission(),permissionSetup.ts:272-285):
- Bash:
isDangerousBashPermission() — 检测脚本解释器、package runner、shell 等
- PowerShell:
isDangerousPowerShellPermission() — 额外检测 iex、Invoke-Command、Start-Process、Add-Type 等 PS 特有的代码执行入口
- Agent/Task:
isDangerousTaskPermission() — 任何 Agent allow 规则都被视为危险,因为子 Agent 可以绕过 Classifier 执行委托攻击(delegation attack)
以 Bash 为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export function isDangerousBashPermission( toolName: string, ruleContent: string | undefined, ): boolean { if (toolName !== BASH_TOOL_NAME) return false
if (ruleContent === undefined || ruleContent === '' || ruleContent === '*') { return true }
for (const pattern of DANGEROUS_BASH_PATTERNS) { if (content === lowerPattern) return true if (content === `${lowerPattern}:*`) return true if (content === `${lowerPattern}*`) return true if (content === `${lowerPattern} *`) return true if (content.startsWith(`${lowerPattern} -`) && content.endsWith('*')) return true } return false }
|
被剥离的规则会暂存在 strippedDangerousRules 中,退出 Auto 模式时通过 restoreDangerousPermissions() 恢复——用户在 default 模式下的 Bash(python:*) 规则不会永久丢失。
危险模式列表包括了所有能执行任意代码的入口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export const CROSS_PLATFORM_CODE_EXEC = [ 'python', 'python3', 'python2', 'node', 'deno', 'tsx', 'ruby', 'perl', 'php', 'lua', 'npx', 'bunx', 'npm run', 'yarn run', 'pnpm run', 'bun run', 'bash', 'sh', 'ssh', ] as const
export const DANGEROUS_BASH_PATTERNS: readonly string[] = [ ...CROSS_PLATFORM_CODE_EXEC, 'zsh', 'fish', 'eval', 'exec', 'env', 'xargs', 'sudo', ]
|
七、Headless Agent 的权限处理
后台运行的 Agent(如 fork 出来的子 Agent、远程调用的 Agent)没有 UI 界面,无法弹出确认对话框。权限系统为这种场景提供了专门的处理路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| async function runPermissionRequestHooksForHeadlessAgent( tool, input, toolUseID, context, ... ): Promise<PermissionDecision | null> { for await (const hookResult of executePermissionRequestHooks( tool.name, toolUseID, input, context, ... )) { if (hookResult.permissionRequestResult?.behavior === 'allow') { return { behavior: 'allow', } } if (hookResult.permissionRequestResult?.behavior === 'deny') { if (decision.interrupt) { context.abortController.abort() } return { behavior: 'deny', } } } return null }
|
这个设计让企业用户可以通过 Hook 脚本实现自定义的权限策略——比如调用内部审批系统、发送 Slack 通知等。
八、可迁移的设计模式
模式 1:防御纵深(Defense in Depth)
权限检查不是单层的”允许/拒绝”,而是多层串联的管线。每层有自己的职责:deny 规则 → 工具内部检查 → safety check → 模式检查 → allow 规则。关键是某些层(safety check、content-specific ask)在高信任模式下仍然生效,但也留有精确的豁免条件——比如 auto 模式下 classifierApprovable 的 safety check 会交给 Classifier 评估,而非一刀切拒绝。
适用场景:任何需要安全控制的系统。将”高风险安全规则”与”可配置的信任级别”分离,但给每层的”不可跳过”规则设计精确的豁免接口,避免过于僵化导致无法工作。
模式 2:渐进式信任 + 快速通道
Auto 模式不是对每个操作都调用昂贵的 Classifier API,而是先通过三层快速通道(acceptEdits 模拟 → 安全工具白名单 → 最后才调 Classifier)过滤掉大部分安全的操作。这让 Classifier 只处理真正需要判断的边缘情况。
适用场景:任何使用 AI 做安全判断的系统。将判断成本与风险级别对齐——低风险操作用规则快速放行,高风险操作才动用昂贵的推理资源。
模式 3:Denial Tracking 与熔断
连续拒绝 3 次或总拒绝 20 次后触发熔断:交互式场景回退到用户确认,headless 场景直接 abort。这避免了 Agent 陷入”尝试-被拒绝-重试”的无效循环。这是一个简单但有效的熔断器模式——当自动化系统持续失败时,根据运行环境选择合适的降级策略。
适用场景:任何 AI Agent 的自动决策系统。当自动判断连续失败时,不应该无限重试——有人在时降级到人工干预,无人在时果断中止。
下一篇预告
第 17 篇:Settings 系统 — 多层配置的合并之道
我们将深入 Claude Code 的多层配置系统——从 local settings 到 enterprise managed settings,6 层配置如何合并、MDM 集成如何工作、配置变更如何被检测和验证。
全部内容请关注 https://github.com/luyao618/Claude-Code-Source-Study (求一颗免费的小星星)