模块五:权限体系 | 前置依赖:第 15 课 | 预计学习时间:75 分钟
学习目标
完成本课后,你将能够:
- 解释 Auto 模式分类器的两阶段判定流程及其 transcript 构建方式
- 说明 YOLO 分类器的系统提示组装与用户自定义规则替换机制
- 列举 Bash 命令的路径遍历防护手段(URL 编码、Unicode、反斜杠、大小写)
- 描述 bashSecurity.ts 中的安全验证链与 shell 元字符检测
- 理解拒绝追踪(Denial Tracking)的熔断机制
16.1 Auto 模式概览
Auto 模式是 Claude Code 最复杂的权限模式。当工具调用需要用户确认时(behavior: 'ask'),auto 模式不弹窗,而是调用 ML 分类器来判定是否安全。
触发条件
// utils/permissions/permissions.ts — hasPermissionsToUseTool
if (
feature('TRANSCRIPT_CLASSIFIER') &&
(appState.toolPermissionContext.mode === 'auto' ||
(appState.toolPermissionContext.mode === 'plan' &&
autoModeStateModule?.isAutoModeActive()))
) {
// 进入 auto 模式分类器流程
}
两个入口:
- 权限模式显式设为
auto - 处于
plan模式但 auto mode 被激活(用于 plan-to-auto 渐进切换)
分类器决策流
工具调用 → ask 决策
│
├─ safetyCheck 且不可自动批准?
│ └─ 跳过分类器 → 保持 ask / deny
│
├─ 工具需要用户交互?(requiresUserInteraction)
│ └─ 跳过分类器 → 保持 ask
│
├─ 检查拒绝追踪阈值
│ └─ 超限? → fallback 到 prompting
│
└─ classifyYoloAction()
│
├─ shouldBlock: false → allow(自动批准)
│ └─ 重置连续拒绝计数
│
├─ shouldBlock: true → deny(自动拒绝)
│ ├─ 记录拒绝
│ ├─ 更新拒绝追踪
│ └─ 发送 UI 通知
│
└─ unavailable → fallback
├─ transcriptTooLong → fallback 到 prompting
└─ API 错误 → 可能 fail-closed
16.2 YOLO 分类器 — Transcript 构建
utils/permissions/yoloClassifier.ts 实现了 auto 模式的核心分类器。
Transcript 条目
分类器需要看到对话历史来理解当前操作的上下文:
export type TranscriptEntry = {
role: 'user' | 'assistant'
content: TranscriptBlock[]
}
type TranscriptBlock =
| { type: 'text'; text: string }
| { type: 'tool_use'; name: string; input: unknown }
buildTranscriptEntries — 提取对话上下文
export function buildTranscriptEntries(messages: Message[]): TranscriptEntry[] {
// 遍历所有消息:
// - user 消息 → 提取文本块
// - assistant 消息 → 只提取 tool_use 块(排除文本!)
// - attachment(queued_command) → 提取为 user 转
//
// 关键安全决策:assistant 文本被排除!
// 因为模型文本可以被精心构造来影响分类器判定
}
toAutoClassifierInput — 工具输入投影
每个工具定义一个 toAutoClassifierInput 方法,将工具输入压缩为分类器需要看到的最小信息:
原始输入:
{
command: "git diff HEAD~3",
timeout: 30000,
description: "..."
}
→ toAutoClassifierInput() →
投影输出:
"git diff HEAD~3" // 只保留命令本身
function toCompactBlock(block, role, lookup): string {
if (block.type === 'tool_use') {
const tool = lookup.get(block.name)
// 安全:try/catch 保护,历史中可能有格式错误的输入
let encoded
try {
encoded = tool.toAutoClassifierInput(input) ?? input
} catch {
encoded = input // fallback 到原始输入
}
if (encoded === '') return '' // 工具返回空串 → 跳过
return `${block.name} ${encoded}\n`
}
if (block.type === 'text' && role === 'user') {
return `User: ${block.text}\n`
}
return ''
}
紧凑 Transcript 格式
User: 请帮我清理临时文件
Bash ls /tmp/my-project/
Bash find /tmp/my-project -name "*.tmp"
User: 好的,删掉它们
Bash rm /tmp/my-project/*.tmp ← 当前要分类的操作
16.3 分类器系统提示
提示组装
export async function buildYoloSystemPrompt(
context: ToolPermissionContext,
): Promise<string> {
// 1. 基础提示(从 .txt 文件加载)
const systemPrompt = BASE_PROMPT.replace(
'<permissions_template>',
() => isUsingExternalPermissions()
? EXTERNAL_PERMISSIONS_TEMPLATE
: ANTHROPIC_PERMISSIONS_TEMPLATE,
)
// 2. 合并 allow/deny 规则
const allowDescriptions = [
...getBashPromptAllowDescriptions(context), // Bash 前缀规则
...(autoMode?.allow ?? []), // 用户自定义 allow
]
const denyDescriptions = [
...getBashPromptDenyDescriptions(context),
...(autoMode?.soft_deny ?? []),
]
// 3. 替换模板中的标记
return systemPrompt
.replace(/<user_allow_rules_to_replace>.../, userAllow ?? defaults)
.replace(/<user_deny_rules_to_replace>.../, userDeny ?? defaults)
.replace(/<user_environment_to_replace>.../, userEnvironment ?? defaults)
}
规则替换机制
用户在 settings.json 的 autoMode 字段可自定义分类器行为:
{
"autoMode": {
"allow": [
"Git commands that only affect local state",
"npm/yarn install, test, build, lint"
],
"soft_deny": [
"Commands that modify system configuration",
"Network requests to unknown hosts"
],
"environment": [
"This is a Node.js project",
"Using PostgreSQL database locally"
]
}
}
这些规则会 替换(外部模板)或 追加(Anthropic 模板)到分类器系统提示中。
CLAUDE.md 注入
分类器还会读取项目的 CLAUDE.md 作为上下文:
function buildClaudeMdMessage(): Anthropic.MessageParam | null {
const claudeMd = getCachedClaudeMdContent()
if (claudeMd === null) return null
return {
role: 'user',
content: [{
type: 'text',
text: `The following is the user's CLAUDE.md configuration...
<user_claude_md>\n${claudeMd}\n</user_claude_md>`,
cache_control: getCacheControl({ querySource: 'auto_mode' }),
}],
}
}
两阶段 XML 分类器
┌──────── Stage 1: Fast Path ────────┐
│ │
│ 发送 transcript + 操作描述 │
│ 追加 S1 后缀 → 要求立即决策 │
│ 模型快速回答 <allow/> 或 <deny/> │
│ │
│ 高置信度? → 直接返回结果 │
│ 低置信度? → 进入 Stage 2 │
└─────────────────────────────────────┘
│ 需要深度思考
▼
┌──────── Stage 2: Thinking ─────────┐
│ │
│ 启用 extended thinking │
│ 模型深度推理后给出判定 │
│ 返回最终结果 │
│ │
└─────────────────────────────────────┘
分类器响应通过工具调用返回结构化结果:
const YOLO_CLASSIFIER_TOOL_SCHEMA = {
name: 'classify_result',
input_schema: {
properties: {
thinking: { type: 'string' }, // 推理过程
shouldBlock: { type: 'boolean' }, // 是否阻止
reason: { type: 'string' }, // 决策原因
},
required: ['thinking', 'shouldBlock', 'reason'],
},
}
16.4 Bash 安全验证链
tools/BashTool/bashSecurity.ts 实现了一条长长的安全验证链,在权限检查之前运行。
验证管线概览
bashCommandIsSafe_DEPRECATED(command)
│
├─ 1. validateEmpty → 空命令直接 allow
├─ 2. validateIncompleteCommands → 片段检测(以 tab/flag/operator 开头)
├─ 3. validateSafeCommandSubstitution → 安全 heredoc 模式
├─ 4. validateJqSystemFunction → jq system() 函数调用
├─ 5. validateJqFileArguments → jq 文件参数
├─ 6. validateObfuscatedFlags → 混淆的命令行标志
├─ 7. validateShellMetacharacters → Shell 元字符
├─ 8. validateDangerousVariables → 危险环境变量
├─ 9. validateNewlines → 命令中的换行符
├─ 10. validateDangerousPatterns → 命令替换、重定向等
├─ 11. validateIfsInjection → IFS 变量注入
├─ 12. validateGitCommitSubstitution → git commit -m 中的替换
├─ 13. validateProcEnvironAccess → /proc/environ 访问
├─ 14. validateMalformedTokens → 畸形 token 注入
├─ 15. validateBackslashEscapedWhitespace → 转义空白字符
├─ 16. validateBraceExpansion → 花括号展开
├─ 17. validateControlCharacters → 控制字符
├─ 18. validateUnicodeWhitespace → Unicode 空白字符
├─ 19. validateMidWordHash → 词中 # 号
├─ 20. validateZshDangerousCommands → Zsh 危险命令
├─ 21. validateBackslashEscapedOperators → 转义运算符
├─ 22. validateCommentQuoteDesync → 注释/引号反同步
└─ 23. validateQuotedNewline → 引号内换行
命令替换检测
这是最关键的检测之一。以下模式全部被阻止:
const COMMAND_SUBSTITUTION_PATTERNS = [
{ pattern: /<\(/, message: 'process substitution <()' },
{ pattern: />\(/, message: 'process substitution >()' },
{ pattern: /=\(/, message: 'Zsh process substitution =()' },
{ pattern: /\$\(/, message: '$() command substitution' },
{ pattern: /\$\{/, message: '${} parameter substitution' },
{ pattern: /\$\[/, message: '$[] legacy arithmetic expansion' },
{ pattern: /~\[/, message: 'Zsh-style parameter expansion' },
// ... 更多模式
]
Zsh 危险命令黑名单
const ZSH_DANGEROUS_COMMANDS = new Set([
'zmodload', // 加载任意模块 → 文件I/O、网络、执行
'emulate', // emulate -c 等价于 eval
'sysopen', // 精细文件控制
'sysread', 'syswrite', 'sysseek',
'zpty', // 伪终端执行
'ztcp', // TCP 连接(可用于数据泄露)
'zsocket', // Unix/TCP socket
'zf_rm', 'zf_mv', 'zf_ln', 'zf_chmod', // 内建文件操作
'zf_chown', 'zf_mkdir', 'zf_rmdir', 'zf_chgrp',
])
16.5 路径遍历防护
tools/BashTool/pathValidation.ts 实现了多层路径安全检查。
路径提取器(PATH_EXTRACTORS)
每个支持路径操作的命令都有专用的路径提取器:
export const PATH_EXTRACTORS: Record<PathCommand, (args: string[]) => string[]> = {
cd: args => (args.length === 0 ? [homedir()] : [args.join(' ')]),
ls: args => { /* 过滤标志,默认 '.' */ },
rm: filterOutFlags,
grep: args => { /* 跳过模式参数,提取路径 */ },
git: args => { /* 只处理 git diff --no-index */ },
// ... 34 个命令
}
POSIX -- 端选项分隔符
这是一个关键安全修复。攻击者可以用 rm -- -/../.claude/settings.json 绕过以 - 开头的路径过滤:
function filterOutFlags(args: string[]): string[] {
const result: string[] = []
let afterDoubleDash = false
for (const arg of args) {
if (afterDoubleDash) {
result.push(arg) // -- 之后的所有参数都是路径
} else if (arg === '--') {
afterDoubleDash = true
} else if (!arg?.startsWith('-')) {
result.push(arg)
}
}
return result
}
路径验证流程
validateCommandPaths(command, args, cwd, context)
│
├─ 提取路径: PATH_EXTRACTORS[command](args)
│
├─ 命令验证器(如有):阻止 mv/cp 的 flag
│
├─ cd + 写操作检查:
│ └─ 复合命令含 cd + write → 阻止
│ // 防止: cd .claude/ && mv test.txt settings.json
│
└─ 对每个路径:
└─ validatePath(path, cwd, context, operationType)
├─ 路径标准化(expandTilde, resolve)
├─ URL 编码检测(%2e%2e → ..)
├─ Unicode 标准化
├─ 反斜杠注入检测
├─ 大小写标准化
├─ 检查是否在允许的工作目录内
├─ 检查是否是受保护文件
└─ 检查 deny 规则
危险删除路径检测
function checkDangerousRemovalPaths(command, args, cwd): PermissionResult {
for (const path of paths) {
const absolutePath = isAbsolute(cleanPath) ? cleanPath : resolve(cwd, cleanPath)
if (isDangerousRemovalPath(absolutePath)) {
return {
behavior: 'ask',
message: `Dangerous ${command} operation detected: '${absolutePath}'
This command would remove a critical system directory.`,
suggestions: [], // 不提供"保存规则"选项!
}
}
}
}
操作类型分类
每个命令被分为三类操作类型,影响权限检查的严格程度:
export const COMMAND_OPERATION_TYPE: Record<PathCommand, FileOperationType> = {
// 读操作 — 最宽松
cat: 'read', ls: 'read', grep: 'read', find: 'read', git: 'read',
// 创建操作 — 中等
mkdir: 'create', touch: 'create',
// 写操作 — 最严格
rm: 'write', mv: 'write', cp: 'write', sed: 'write',
}
16.6 拒绝追踪(Denial Tracking)
utils/permissions/denialTracking.ts 实现了一个简洁但关键的熔断机制。
状态结构
export type DenialTrackingState = {
consecutiveDenials: number // 连续拒绝次数
totalDenials: number // 总拒绝次数
}
export const DENIAL_LIMITS = {
maxConsecutive: 3, // 连续 3 次拒绝 → 熔断
maxTotal: 20, // 总计 20 次拒绝 → 熔断
} as const
不可变状态更新
export function recordDenial(state: DenialTrackingState): DenialTrackingState {
return {
...state, // 不可变更新
consecutiveDenials: state.consecutiveDenials + 1,
totalDenials: state.totalDenials + 1,
}
}
export function recordSuccess(state: DenialTrackingState): DenialTrackingState {
if (state.consecutiveDenials === 0) return state // 无变化 → 返回同引用
return {
...state,
consecutiveDenials: 0, // 成功重置连续计数,但不重置总数
}
}
熔断条件
export function shouldFallbackToPrompting(state: DenialTrackingState): boolean {
return (
state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive ||
state.totalDenials >= DENIAL_LIMITS.maxTotal
)
}
熔断流程
auto 模式运行中...
操作 1: 分类器拒绝 → consecutive=1, total=1
操作 2: 分类器拒绝 → consecutive=2, total=2
操作 3: 分类器拒绝 → consecutive=3, total=3
shouldFallbackToPrompting() → true!
┌─────────────────────────────────┐
│ 熔断触发:回退到用户交互模式 │
│ 不再调用分类器,直接弹窗询问 │
└─────────────────────────────────┘
用户手动批准 → consecutive 重置为 0
恢复 auto 模式分类器...
或者总拒绝达标后:
... 各种操作,累计 20 次拒绝 ...
shouldFallbackToPrompting() → true!(永久熔断直到会话重启)
设计意图
这个机制防止以下问题:
- 死循环: 模型反复尝试被拒绝的操作
- 用户无感知: auto 模式下用户看不到拒绝,不知道模型在"空转"
- 资源浪费: 每次分类器调用都消耗 API token
16.7 引号提取与安全分析
bashSecurity.ts 中一个精妙的函数用于提取引号外的内容进行安全检查:
function extractQuotedContent(command: string, isJq = false): QuoteExtraction {
let withDoubleQuotes = '' // 保留双引号内容
let fullyUnquoted = '' // 完全去引号
let unquotedKeepQuoteChars = '' // 去内容但保留引号字符
let inSingleQuote = false
let inDoubleQuote = false
let escaped = false
for (let i = 0; i < command.length; i++) {
const char = command[i]
if (escaped) {
escaped = false
if (!inSingleQuote) withDoubleQuotes += char
if (!inSingleQuote && !inDoubleQuote) fullyUnquoted += char
continue
}
if (char === '\\' && !inSingleQuote) {
escaped = true
// ... 传递转义字符
continue
}
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote
unquotedKeepQuoteChars += char // 保留引号字符本身
continue
}
// ... 双引号类似处理
}
return { withDoubleQuotes, fullyUnquoted, unquotedKeepQuoteChars }
}
三种提取模式的用途:
原始命令: echo 'safe text' "rm -rf /" `date`
withDoubleQuotes: echo "rm -rf /" `date`
↑ 单引号内容被移除,检测双引号和反引号注入
fullyUnquoted: echo `date`
↑ 所有引号内容移除,检测裸露的命令替换
unquotedKeepQuoteChars: echo '' '' `date`
↑ 保留引号字符,检测 'x'# 等引号邻接模式
安全重定向剥离
function stripSafeRedirections(content: string): string {
return content
.replace(/\s+2\s*>&\s*1(?=\s|$)/g, '') // 2>&1
.replace(/[012]?\s*>\s*\/dev\/null(?=\s|$)/g, '') // > /dev/null
.replace(/\s*<\s*\/dev\/null(?=\s|$)/g, '') // < /dev/null
}
// 注意:每个模式都有 (?=\s|$) 边界检查
// 防止 > /dev/nullo 被误匹配为 > /dev/null + 'o'
16.8 安全 Heredoc 检测
isSafeHeredoc 是一个精密的函数,判断 $(cat <<'EOF'...EOF) 模式是否安全:
安全模式:
git commit -m "$(cat <<'EOF'
这是提交信息
EOF
)"
检查条件:
1. 分隔符必须被单引号/转义 → 内容是字面量
2. 结束分隔符必须独占一行 → 精确匹配 bash 行为
3. $() 不能在命令名位置 → 防止 heredoc 内容成为命令
4. 剩余文本只能包含安全字符
5. 不能有嵌套 heredoc → 防止索引破坏攻击
6. 剩余文本必须通过所有其他验证器
这个函数是一个 EARLY-ALLOW 路径 — 返回 true 会绕过所有后续验证器。因此检查极其严格。
课后练习
练习 1:分类器 Transcript 构建
给定以下对话历史,手动构建分类器看到的 transcript:
用户: "帮我部署到生产环境"
模型: [text: "好的,我来执行部署"] [tool_use: Bash("npm run build")]
模型: [tool_use: Bash("npm run deploy:prod")] ← 当前操作
思考:为什么 assistant 文本被排除?
练习 2:路径攻击分析
分析以下攻击向量,说明 pathValidation.ts 如何防御:
rm -- -/../.claude/settings.jsoncat %2e%2e/%2e%2e/etc/passwdls .cLauDe/Settings.locaL.jsoncd .claude/ && mv test.txt settings.json
练习 3:拒绝追踪状态机
画出 DenialTrackingState 的状态转换图,标注:
- 初始状态 (0, 0)
- 连续拒绝 3 次后的状态
- 中间成功一次后的状态
- 总计达到 20 次后的状态
练习 4:安全验证绕过
阅读 bashCommandIsSafe_DEPRECATED 的 22 个验证器。找出一个"先返回的验证器会掩盖后续验证器"的场景。思考这是否构成安全问题。
本课小结
| 要点 | 内容 |
|---|---|
| Auto 模式 | feature-gated ML 分类器,两阶段判定(fast + thinking) |
| Transcript | 用户文本 + 助手工具调用(排除助手文本),JSONL 紧凑格式 |
| 系统提示 | BASE_PROMPT + 权限模板 + 用户自定义 allow/deny/environment |
| Bash 安全 | 22+ 验证器链,覆盖命令替换、元字符、Zsh 模块等 |
| 路径防护 | URL 编码、Unicode、反斜杠、大小写标准化、-- 分隔符 |
| 拒绝追踪 | 连续 3 次或总计 20 次 → 熔断回退到用户交互 |
| 引号提取 | 三模式(保留双引号、完全去引号、保留引号字符)精确分析 |
下一课预告
第 17 课:Prompt 组装架构 — 深入 constants/prompts.ts 的 getSystemPrompt() 函数,理解静态节(可缓存)与动态节(按需计算)的分界线、缓存策略、以及 System Prompt 如何被拆分为数组发送给 API。