4.5 共享安全逻辑 — 跨工具权限检查与参数验证的工程实现
对应原书:第4章 4.3节"Fail-Closed 的类型安全设计"(安全默认值哲学)、4.5节"九步执行管线"(权限决策步骤)、4.7.1节"安全默认值"哲学、4.12节"设计模式提炼"
辅助源码:tools/shared/(gitOperationTracking.ts 278行、spawnMultiAgent.ts 1094行)、utils/permissions/(filesystem.ts 1778行、permissions.ts 1487行、PermissionMode.ts 142行、PermissionRule.ts 41行、permissionRuleParser.ts 199行、dangerousPatterns.ts 81行、denialTracking.ts 46行、shadowedRuleDetection.ts 235行)
重点关注:跨工具权限决策管线、6 种权限模式、规则匹配引擎(gitignore 语义)、路径安全共享检查、规则解析与转义、影子规则检测、拒绝追踪状态机、危险模式过滤、Git 操作追踪、多代理生成权限继承
1. 导语:tools/shared/ 的双重含义
原书 4.3.1 节阐述了 Fail-Closed 的安全默认值哲学:isConcurrencySafe: () => false(保守假设不安全)、isReadonly: () => false(保守假设会写入)、checkPermissions: allow(默认需权限检查)。这些默认值体现了"宁可多弹一次权限确认,不可漏放一次危险操作"的原则。
tools/shared/ 目录在源码中包含两个文件,但"共享安全逻辑"的内涵远超此目录。跨工具共享的安全基础设施主要分布在 utils/permissions/ 目录下,构成所有工具权限检查的公共底座:
┌─────────────────────────────────────────────────────────────────────┐
│ 共享安全逻辑架构总览 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ tools/shared/ utils/permissions/ │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ gitOperationTracking │ │ permissions.ts │ │
│ │ .ts (278行) │ │ hasPermissionsToUseTool │ │
│ │ – Git 操作检测 │ │ checkRuleBasedPerms │ │
│ │ – PR/commit 追踪 │ │ 规则匹配引擎 │ │
│ ├──────────────────────┤ ├──────────────────────────┤ │
│ │ spawnMultiAgent.ts │ │ filesystem.ts (1778行) │ │
│ │ (1094行) │ │ checkReadPermission │ │
│ │ – 队友生成 │ │ checkWritePermission │ │
│ │ – 权限继承 │ │ checkPathSafety │ │
│ │ – 沙箱/Plan模式 │ │ matchingRuleForInput │ │
│ └──────────────────────┘ ├──────────────────────────┤ │
│ │ PermissionMode.ts │ │
│ │ 6种权限模式 │ │
│ ├──────────────────────────┤ │
│ │ permissionRuleParser.ts │ │
│ │ 规则解析+转义 │ │
│ ├──────────────────────────┤ │
│ │ shadowedRuleDetection.ts │ │
│ │ 影子规则检测 │ │
│ ├──────────────────────────┤ │
│ │ denialTracking.ts │ │
│ │ 拒绝追踪状态机 │ │
│ ├──────────────────────────┤ │
│ │ dangerousPatterns.ts │ │
│ │ 危险模式过滤 │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
2. 权限模式系统(PermissionMode.ts)
2.1 六种权限模式
PermissionMode.ts 定义了 6 种权限模式,构成工具执行安全策略的模式空间:
const PERMISSION_MODE_CONFIG: Partial<Record<PermissionMode, PermissionModeConfig>> = {
default: { title: 'Default', symbol: '', color: 'text' },
plan: { title: 'Plan Mode', symbol: '⏸', color: 'planMode' },
acceptEdits: { title: 'Accept edits', symbol: '⏵⏵', color: 'autoAccept' },
bypassPermissions:{ title: 'Bypass Permissions', symbol: '⏵⏵', color: 'error' },
dontAsk: { title: "Don't Ask", symbol: '⏵⏵', color: 'error' },
// 条件编译:仅 TRANSCRIPT_CLASSIFIER feature 开启时可用
auto: { title: 'Auto mode', symbol: '⏵⏵', color: 'warning' },
}
模式安全等级:
| plan | 最高 | 只允许只读工具,禁止一切写入 |
| default | 高 | 遵循 deny → ask → allow 规则链 |
| acceptEdits | 中 | 工作目录内文件编辑自动放行 |
| auto | 中 | AI 分类器自动审批(内部功能) |
| dontAsk | 低 | 将所有 ask 转为 deny(静默拒绝) |
| bypassPermissions | 最低 | 跳过所有权限检查(危险) |
2.2 内部模式 vs 外部模式
export function isExternalPermissionMode(mode: PermissionMode): mode is ExternalPermissionMode {
if (process.env.USER_TYPE !== 'ant') return true; // 外部用户不能有 auto
return mode !== 'auto' && mode !== 'bubble';
}
auto 和 bubble 是 Anthropic 内部(ant)专用模式,外部用户不可用。这通过 USER_TYPE 环境变量在运行时过滤。
3. 权限规则系统(PermissionRule.ts + permissionRuleParser.ts)
3.1 规则的三种行为
// PermissionRule.ts
export const permissionBehaviorSchema = lazySchema(() =>
z.enum(['allow', 'deny', 'ask']),
)
- allow — 允许工具运行,不提示用户
- deny — 拒绝工具运行,不可覆盖
- ask — 强制弹出审批提示
3.2 规则字符串解析
规则格式为 ToolName 或 ToolName(content)。permissionRuleParser.ts 实现了安全的解析器:
// 解析示例
permissionRuleValueFromString('Bash') // => { toolName: 'Bash' }
permissionRuleValueFromString('Bash(npm install)') // => { toolName: 'Bash', ruleContent: 'npm install' }
permissionRuleValueFromString('Bash(python -c "print\\\\(1\\\\)")') // => { toolName: 'Bash', ruleContent: 'python -c "print(1)"' }
转义处理是关键安全特性。规则内容中的括号必须转义,否则会破坏解析:
export function escapeRuleContent(content: string): string {
return content
.replace(/\\\\/g, '\\\\\\\\') // 1. 先转义反斜杠
.replace(/\\(/g, '\\\\(') // 2. 再转义左括号
.replace(/\\)/g, '\\\\)') // 3. 最后转义右括号
}
// 反序操作:先解括号,后解反斜杠
export function unescapeRuleContent(content: string): string {
return content
.replace(/\\\\\\(/g, '(') // 1. 先解括号
.replace(/\\\\\\)/g, ')')
.replace(/\\\\\\\\/g, '\\\\') // 2. 后解反斜杠
}
解析器使用 findFirstUnescapedChar / findLastUnescapedChar 进行括号匹配,通过计算前导反斜杠数量(奇数为转义,偶数为未转义)来定位真正的分隔符。
3.3 遗留工具名归一化
const LEGACY_TOOL_NAME_ALIASES: Record<string, string> = {
Task: AGENT_TOOL_NAME, // Task → Agent
KillShell: TASK_STOP_TOOL_NAME, // KillShell → TaskStop
AgentOutputTool: TASK_OUTPUT_TOOL_NAME, // → TaskOutput
BashOutputTool: TASK_OUTPUT_TOOL_NAME, // → TaskOutput
}
工具重命名后,旧规则字符串自动映射到新名称,保证向后兼容。
3.4 规则来源优先级
const PERMISSION_RULE_SOURCES = [
…SETTING_SOURCES, // policySettings > userSettings > projectSettings > localSettings > flagSettings
'cliArg', // 命令行参数
'command', // slash command frontmatter
'session', // 会话内存
] as const
规则按来源分层:企业策略(policySettings)优先级最高,会话级(session)最低。高优先级来源的 deny 规则不可被低优先级来源的 allow 规则覆盖。
4. 权限决策管线(permissions.ts)— 核心引擎
4.1 hasPermissionsToUseTool — 主决策入口
hasPermissionsToUseTool 是所有工具权限检查的统一入口,实现了原书 4.5 节描述的"九步执行管线"中的权限决策部分:
export const hasPermissionsToUseTool: CanUseToolFn = async (
tool, input, context, assistantMessage, toolUseID
): Promise<PermissionDecision> => {
// 第一阶段:规则检查
const result = await hasPermissionsToUseToolInner(tool, input, context)
// 第二阶段:模式后处理
if (result.behavior === 'allow') {
// auto 模式下重置连续拒绝计数
if (mode === 'auto' && denialState.consecutiveDenials > 0) {
persistDenialState(context, recordSuccess(denialState))
}
return result
}
if (result.behavior === 'ask') {
// dontAsk 模式:ask → deny
if (mode === 'dontAsk') return { behavior: 'deny', … }
// auto 模式:AI 分类器审批
if (mode === 'auto') {
// 1. 安全检查不可分类的 → 保持 ask
// 2. acceptEdits 快速路径 → allow
// 3. 安全工具白名单 → allow
// 4. YOLO 分类器 → allow/deny
// 5. 拒绝限制检查 → 回退到提示
}
// 无头代理:PermissionRequest Hook → deny
if (shouldAvoidPermissionPrompts) {
const hookDecision = await runPermissionRequestHooksForHeadlessAgent(…)
return hookDecision ?? { behavior: 'deny', … }
}
}
return result
}
4.2 hasPermissionsToUseToolInner — 七步规则检查
async function hasPermissionsToUseToolInner(tool, input, context): Promise<PermissionDecision> {
// 1a. 整个工具被 deny 规则拒绝
const denyRule = getDenyRuleForTool(context.toolPermissionContext, tool)
if (denyRule) return { behavior: 'deny', … }
// 1b. 整个工具有 ask 规则
const askRule = getAskRuleForTool(context.toolPermissionContext, tool)
if (askRule) {
// 沙箱自动放行例外:Bash + 沙箱启用 + 命令可沙箱化
const canSandboxAutoAllow = tool.name === 'Bash' &&
SandboxManager.isSandboxingEnabled() &&
SandboxManager.isAutoAllowBashIfSandboxedEnabled() &&
shouldUseSandbox(input)
if (!canSandboxAutoAllow) return { behavior: 'ask', … }
}
// 1c. 工具自定义权限检查 (tool.checkPermissions)
const parsedInput = tool.inputSchema.parse(input)
const toolPermissionResult = await tool.checkPermissions(parsedInput, context)
// 1d. 工具实现拒绝
if (toolPermissionResult?.behavior === 'deny') return toolPermissionResult
// 1e. 需要用户交互的工具(即使 bypass 模式也要提示)
if (tool.requiresUserInteraction?.() && toolPermissionResult?.behavior === 'ask')
return toolPermissionResult
// 1f. 内容特定的 ask 规则(如 Bash(npm publish:*))
if (toolPermissionResult?.behavior === 'ask' &&
toolPermissionResult.decisionReason?.type === 'rule' &&
toolPermissionResult.decisionReason.rule.ruleBehavior === 'ask')
return toolPermissionResult
// 1g. 安全检查 bypass 免疫(.git/、.claude/、.vscode/ 等)
if (toolPermissionResult?.behavior === 'ask' &&
toolPermissionResult.decisionReason?.type === 'safetyCheck')
return toolPermissionResult
// 2a. bypassPermissions 模式放行
if (shouldBypassPermissions) return { behavior: 'allow', … }
// 2b. 整个工具有 allow 规则
const alwaysAllowedRule = toolAlwaysAllowedRule(context.toolPermissionContext, tool)
if (alwaysAllowedRule) return { behavior: 'allow', … }
// 3. passthrough → ask
return { behavior: 'ask', … }
}
关键设计:bypass 免疫检查
步骤 1f 和 1g 是源码独有的安全设计:
- 1f — 内容特定的 ask 规则(如 Bash(npm publish:*))即使在 bypassPermissions 模式下也必须遵守
- 1g — 安全检查(.git/、.claude/、.vscode/、shell 配置文件等敏感路径)即使在 bypassPermissions 模式下也必须提示
这两个检查在 bypassPermissions 放行(步骤 2a)之前执行,确保安全底线不被绕过。
4.3 checkRuleBasedPermissions — bypass 感知的规则检查子集
export async function checkRuleBasedPermissions(tool, input, context) {
// 仅执行步骤 1a-1g(bypass 前的规则检查)
// 不执行 auto 分类器、模式转换、Hook、bypass/allow 放行
// 用于 PreToolUse Hook 的 bypass 安全检查
}
这个函数让 Hook 也能执行与主管线一致的规则检查,即使 Hook 返回 allow,安全检查仍然生效。
5. 文件系统权限引擎(filesystem.ts)— 共享底座
5.1 checkWritePermissionForTool — 八步写权限流水线
这是 FileEditTool、FileWriteTool 等所有写入工具共享的权限决策函数:
步骤 1: deny 规则检查(含符号链接双重检查)
步骤 1.5: 内部可编辑路径放行(plan 文件、scratchpad、agent memory)
步骤 1.6: .claude/** 会话级 allow 规则旁路
步骤 1.7: 综合安全验证(Windows 路径 + Claude 配置 + 危险文件/目录)
步骤 2: ask 规则检查
步骤 3: acceptEdits 模式放行(工作目录内)
步骤 4: allow 规则检查
步骤 5: 默认询问
5.2 checkReadPermissionForTool — 八步读权限流水线
步骤 1: UNC 路径防御性检查
步骤 2: Windows 可疑路径模式检查
步骤 3: READ-SPECIFIC deny 规则(优先于编辑权限)
步骤 4: READ-SPECIFIC ask 规则
步骤 5: 编辑权限 implies 读权限(编辑 allow → 读 allow)
步骤 6: 工作目录内读取放行
步骤 7: 内部可读路径放行(session memory、plans、tool results、scratchpad)
步骤 8: allow 规则检查
步骤 9: 默认询问
关键设计:读权限的安全优先级
步骤 3(read deny)必须在步骤 5(edit implies read)之前执行,否则显式的 read deny 规则会被 edit allow 规则绕过。源码注释明确标注:
// SECURITY: This must come before any allow checks (including "edit access implies read access")
// to prevent bypassing explicit read deny rules
5.3 规则匹配引擎 — gitignore 语义
matchingRuleForInput 使用 ignore 库实现 gitignore 风格的模式匹配:
export function matchingRuleForInput(
path: string,
toolPermissionContext: ToolPermissionContext,
toolType: 'edit' | 'read',
behavior: 'allow' | 'deny' | 'ask',
): PermissionRule | null {
// 1. 将路径转换为 POSIX 格式(跨平台一致性)
// 2. 按根路径分组规则(null = 匹配任意位置,~/ = homedir,/ = 绝对路径)
// 3. 对每个根计算相对路径
// 4. 使用 ignore 库匹配
// 5. 返回匹配的 PermissionRule
}
路径前缀语义:
| // | /(文件系统根) | //etc/passwd |
| ~/ | homedir() | ~/.ssh/config |
| / | 设置文件所在目录 | /.env |
| 无前缀 | null(匹配任意位置) | .env |
5.4 路径安全检查 — checkPathSafetyForAutoEdit
这是跨工具共享的路径安全检查函数,在 checkWritePermissionForTool 步骤 1.7 中调用:
export function checkPathSafetyForAutoEdit(path: string):
| { safe: true }
| { safe: false; message: string; classifierApprovable: boolean }
{
const pathsToCheck = getPathsForPermissionCheck(path) // 原始路径 + 符号链接解析路径
// 检查 1: Windows 可疑路径模式(7 种)
for (const pathToCheck of pathsToCheck) {
if (hasSuspiciousWindowsPathPattern(pathToCheck)) return { safe: false, … }
}
// 检查 2: Claude 配置文件(settings.json、commands/、agents/、skills/)
for (const pathToCheck of pathsToCheck) {
if (isClaudeConfigFilePath(pathToCheck)) return { safe: false, … }
}
// 检查 3: 危险文件/目录
for (const pathToCheck of pathsToCheck) {
if (isDangerousFilePathToAutoEdit(pathToCheck)) return { safe: false, … }
}
return { safe: true }
}
7 种 Windows 路径攻击模式(hasSuspiciousWindowsPathPattern):
危险文件列表:
export const DANGEROUS_FILES = [
'.gitconfig', '.gitmodules', // Git 配置
'.bashrc', '.bash_profile', // Bash 配置
'.zshrc', '.zprofile', '.profile', // Shell 配置
'.ripgreprc', // ripgrep 配置
'.mcp.json', '.claude.json', // Claude 配置
] as const
export const DANGEROUS_DIRECTORIES = [
'.git', // Git 仓库元数据
'.vscode', // VS Code 配置
'.idea', // JetBrains 配置
'.claude', // Claude 配置
] as const
5.5 工作目录验证 — pathInAllowedWorkingPath
export function pathInAllowedWorkingPath(
path: string,
toolPermissionContext: ToolPermissionContext,
): boolean {
const pathsToCheck = getPathsForPermissionCheck(path) // 原始 + 符号链接解析
const workingPaths = Array.from(allWorkingDirectories(toolPermissionContext))
.flatMap(wp => getResolvedWorkingDirPaths(wp)) // 工作目录也解析符号链接
// 所有路径变体都必须在某个工作目录内
return pathsToCheck.every(pathToCheck =>
workingPaths.some(workingPath => pathInWorkingPath(pathToCheck, workingPath))
)
}
pathInWorkingPath 的三重防御:
5.6 内部路径放行
checkEditableInternalPath 和 checkReadableInternalPath 为 Claude Code 自身管理的路径提供免权限放行:
可编辑内部路径:
- 当前会话的 plan 文件({plansDir}/{planSlug}.md)
- 当前会话的 scratchpad 目录
- Agent memory 目录
- Auto memory 目录(~/.claude/ 下,免 DANGEROUS_DIRECTORIES 检查)
- Job 目录(TEMPLATES feature,含劫持防护)
- .claude/launch.json(桌面预览配置)
可读内部路径(额外包含):
- Session memory 目录
- Project 目录(~/.claude/projects/{sanitized-cwd}/)
- Tool results 目录(持久化大输出)
- Project temp 目录(/tmp/claude-{uid}/{sanitized-cwd}/)
- Tasks 目录(~/.claude/tasks/)
- Teams 目录(~/.claude/teams/)
- Bundled skills 根目录(含随机 nonce 防护)
关键安全设计:Bundled Skills 的 Nonce 防护
export const getBundledSkillsRoot = memoize(function getBundledSkillsRoot(): string {
const nonce = randomBytes(16).toString('hex') // 每进程随机 16 字节
return join(getClaudeTempDir(), 'bundled-skills', MACRO.VERSION, nonce)
})
源码注释明确指出 nonce 是"load-bearing defense"(承重防御):其他路径组件(uid、VERSION、skill 名)都是公开已知的,没有 nonce 就可以在共享 /tmp 上预创建目录树进行符号链接攻击。
5.7 大小写不敏感比较
export function normalizeCaseForComparison(path: string): string {
return path.toLowerCase()
}
这个函数在所有路径安全检查中使用,防止在大小写不敏感文件系统(macOS/Windows)上通过混合大小写路径绕过安全检查(如 .cLauDe/Settings.locaL.json)。
6. 拒绝追踪状态机(denialTracking.ts)
6.1 状态定义
export type DenialTrackingState = {
consecutiveDenials: number // 连续拒绝次数
totalDenials: number // 总拒绝次数
}
export const DENIAL_LIMITS = {
maxConsecutive: 3, // 连续拒绝 3 次后回退到交互提示
maxTotal: 20, // 总拒绝 20 次后回退到交互提示
} as const
6.2 状态转换
┌──────────────────────────┐
│ createDenialTrackingState│
│ consecutive: 0, total: 0│
└────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ recordDenial(state) │
┌─────│ consecutive++, total++ │─────┐
│ └────────────┬─────────────┘ │
│ │ │
│ consecutive │ │ total >= 20
│ >= 3 │ │ 或
│ │ │ consecutive >= 3
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ shouldFallbackToPrompting │◄────┘
│ │ = true │
│ └────────────┬─────────────┘
│ │
│ ┌────────────▼─────────────┐
│ │ handleDenialLimitExceeded │
│ │ → 回退到交互提示 │
│ │ → headless 模式抛 Abort │
│ └──────────────────────────┘
│
│ ┌──────────────────────────┐
└────►│ recordSuccess(state) │
│ consecutive = 0 │
│ (total 不变) │
└──────────────────────────┘
6.3 无头模式下的处理
当 shouldAvoidPermissionPrompts 为 true(无头代理)且达到拒绝限制时:
if (isHeadless) {
throw new AbortError('Agent aborted: too many classifier denials in headless mode')
}
无头代理无法弹出交互提示,直接中止整个代理。
7. 影子规则检测(shadowedRuleDetection.ts)
7.1 问题场景
当存在工具级 ask/deny 规则和内容级 allow 规则时,allow 规则永远不会被触达:
ask 规则: Bash(工具级,无 content)
allow 规则: Bash(ls:*)(内容级)
→ ask 规则在步骤 1b 触发,allow 规则永远到不了步骤 2b
→ allow 规则被"影子化"(shadowed),不可达
7.2 检测逻辑
export function detectUnreachableRules(
context: ToolPermissionContext,
options: DetectUnreachableRulesOptions,
): UnreachableRule[] {
// deny 影子(更严重 — 完全阻断)
// allow 规则被工具级 deny 规则完全阻断
// ask 影子(较轻 — 总是提示)
// allow 规则被工具级 ask 规则覆盖,用户总被提示
}
7.3 沙箱例外
Bash + 沙箱启用时,工具级 ask 规则(来自个人设置)不会影子化内容级 allow 规则,因为沙箱命令会自动放行。但来自共享设置(projectSettings、policySettings)的 ask 规则仍然会影子化,因为其他团队成员可能没有沙箱。
function isSharedSettingSource(source: PermissionRuleSource): boolean {
return source === 'projectSettings' || source === 'policySettings' || source === 'command'
}
8. 危险模式过滤(dangerousPatterns.ts)
8.1 跨平台代码执行入口
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',
// Shell
'bash', 'sh',
// 远程命令
'ssh',
] as const
8.2 Bash 特有危险模式
export const DANGEROUS_BASH_PATTERNS: readonly string[] = [
…CROSS_PLATFORM_CODE_EXEC,
'zsh', 'fish', // 额外 Shell
'eval', 'exec', // 代码执行
'env', // 环境变量注入
'xargs', // 命令构建
'sudo', // 提权
// ant 内部工具(仅 Anthropic 内部)
…(process.env.USER_TYPE === 'ant'
? ['fa run', 'coo', 'gh', 'gh api', 'curl', 'wget', 'git', 'kubectl', 'aws', 'gcloud', 'gsutil']
: []),
]
这些模式用于 permissionSetup.ts 的 isDangerousBashPermission 检查,在进入 auto 模式时剥离这些宽泛的 allow 规则,防止通过 Bash(python:*) 等规则绕过分类器。
9. Git 操作追踪(tools/shared/gitOperationTracking.ts)
9.1 Shell 无关的 Git 操作检测
gitOperationTracking.ts 是 tools/shared/ 目录的第一个文件,实现了跨 Shell(Bash/PowerShell)的 Git 操作检测:
function gitCmdRe(subcmd: string, suffix = ''): RegExp {
return new RegExp(
`\\\\bgit(?:\\\\s+-[cC]\\\\s+\\\\S+|\\\\s+–\\\\S+=\\\\S+)*\\\\s+${subcmd}\\\\b${suffix}`,
)
}
这个正则容忍 git 全局选项(-c key=val、-C path、–git-dir=path),因为模型在签名失败后常重试 git -c commit.gpgsign=false commit。
9.2 检测的操作类型
| git commit | GIT_COMMIT_RE | SHA([branch sha] 格式)+ amend 检测 |
| git push | GIT_PUSH_RE | 分支名(ref 更新行解析) |
| git cherry-pick | GIT_CHERRY_PICK_RE | SHA + cherry-pick 标记 |
| git merge | GIT_MERGE_RE | 目标 ref + Fast-forward/Merge made 检测 |
| git rebase | GIT_REBASE_RE | 目标 ref + Successfully rebased 检测 |
| gh pr create/edit/merge/comment/close/ready | GH_PR_ACTIONS | PR 编号 + URL + 操作类型 |
| glab mr create | 正则匹配 | PR 计数 |
| curl POST + PR endpoint | 组合检测 | REST API PR 创建 |
9.3 分析事件追踪
export function trackGitOperations(command: string, exitCode: number, stdout?: string): void {
if (exitCode !== 0) return // 仅追踪成功操作
if (GIT_COMMIT_RE.test(command)) {
logEvent('tengu_git_operation', { operation: 'commit' })
if (command.match(/–amend\\b/))
logEvent('tengu_git_operation', { operation: 'commit_amend' })
getCommitCounter()?.add(1)
}
// … push, PR 等类似
}
gh pr create 成功时还会自动将会话链接到 PR:
if (prHit?.action === 'created' && stdout) {
const prInfo = findPrInStdout(stdout)
if (prInfo) {
void import('../../utils/sessionStorage.js').then(({ linkSessionToPR }) => {
void linkSessionToPR(sessionId, prInfo.prNumber, prInfo.prUrl, prInfo.prRepository)
})
}
}
detectGitOperation 函数用于折叠的工具使用摘要(“committed a1b2c3, created PR #42, ran 3 bash commands”),它检查命令文本以避免匹配输出中出现的 SHA/URL。
10. 多代理生成(tools/shared/spawnMultiAgent.ts)
10.1 共享生成入口
spawnMultiAgent.ts 是 tools/shared/ 目录的第二个文件,从 TeammateTool 中提取以供 AgentTool 复用:
export async function spawnTeammate(
config: SpawnTeammateConfig,
context: ToolUseContext,
): Promise<{ data: SpawnOutput }> {
return handleSpawn(config, context)
}
10.2 三种生成模式
handleSpawn()
│
├── isInProcessEnabled()?
│ └── YES → handleSpawnInProcess() // 进程内(AsyncLocalStorage)
│
├── detectAndGetBackend() 失败?
│ └── auto 模式 → 回退到 handleSpawnInProcess()
│ └── 非 auto 模式 → 抛出错误
│
└── 后端可用
├── use_splitpane !== false → handleSpawnSplitPane() // 分屏
└── else → handleSpawnSeparateWindow() // 独立窗口
10.3 权限继承 — buildInheritedCliFlags
队友生成时的权限继承是核心安全逻辑:
function buildInheritedCliFlags(options?: {
planModeRequired?: boolean
permissionMode?: PermissionMode
}): string {
const flags: string[] = []
if (planModeRequired) {
// Plan 模式优先 — 不继承 bypass 权限
} else if (permissionMode === 'bypassPermissions' || getSessionBypassPermissionsMode()) {
flags.push('–dangerously-skip-permissions')
} else if (permissionMode === 'acceptEdits') {
flags.push('–permission-mode acceptEdits')
} else if (permissionMode === 'auto') {
flags.push('–permission-mode auto')
}
// 继承 –model、–settings、–plugin-dir、–chrome 等
return flags.join(' ')
}
关键安全设计:Plan 模式优先
当 planModeRequired 为 true 时,不继承 bypassPermissions 权限。Plan 模式的安全级别高于 bypass,确保计划审批的完整性。
10.4 名称消毒
const sanitizedName = sanitizeAgentName(uniqueName)
const teammateId = formatAgentId(sanitizedName, teamName)
sanitizeAgentName 防止 @ 字符出现在代理 ID 中(会破坏 agentName@teamName 格式)。generateUniqueTeammateName 检查现有团队成员,自动添加数字后缀(tester-2、tester-3)。
10.5 命令注入防护
所有用户输入在拼接为 shell 命令前都经过 quote() 处理:
const spawnCommand = `cd ${quote([workingDir])} && env ${envStr} ${quote([binaryPath])} ${teammateArgs}${flagsStr}`
quote() 来自 utils/bash/shellQuote.ts,确保路径和参数中的特殊字符被正确转义。
11. Auto 模式分类器集成(permissions.ts)
11.1 分类器决策流程
当权限模式为 auto 且规则检查返回 ask 时,进入 AI 分类器决策:
ask 结果
│
├── 安全检查不可分类 (classifierApprovable: false)?
│ └── 保持 ask(安全检查必须人工审批)
│
├── 工具需要用户交互?
│ └── 保持 ask
│
├── PowerShell 且未启用 POWERSHELL_AUTO_MODE?
│ └── 保持 ask
│
├── acceptEdits 快速路径
│ └── 用 acceptEdits 模式重新检查 → allow?
│ └── allow(跳过分类器 API 调用)
│
├── 安全工具白名单
│ └── isAutoModeAllowlistedTool(tool.name)?
│ └── allow(跳过分类器 API 调用)
│
└── YOLO 分类器
├── shouldBlock = false → allow
├── shouldBlock = true, unavailable → fail closed/open
├── shouldBlock = true, transcriptTooLong → 回退到提示
└── shouldBlock = true → deny + 拒绝追踪
11.2 Iron Gate 机制
分类器不可用时(API 错误),行为由 tengu_iron_gate_closed 特性门控:
if (classifierResult.unavailable) {
if (getFeatureValue_CACHED_WITH_REFRESH('tengu_iron_gate_closed', true, 30 * 60 * 1000)) {
// Fail closed — 拒绝并提示重试
return { behavior: 'deny', message: buildClassifierUnavailableMessage(…) }
}
// Fail open — 回退到正常权限处理
return result
}
默认 true(fail closed),30 分钟刷新一次。这是一个安全可配置的熔断器。
11.3 安全检查的 bypass 免疫
// classifierApprovable: false 的安全检查对所有自动审批路径免疫:
// – acceptEdits 快速路径
// – 安全工具白名单
// – 分类器
if (result.decisionReason?.type === 'safetyCheck' && !result.decisionReason.classifierApprovable) {
if (shouldAvoidPermissionPrompts) {
return { behavior: 'deny', … } // 无头模式拒绝
}
return result // 保持 ask,必须人工审批
}
classifierApprovable 区分两种安全检查:
- false — Windows 路径模式(必须人工审批,分类器不可审批)
- true — Claude 配置文件/危险文件(分类器可审批)
12. 权限决策原因追踪
12.1 PermissionDecisionReason 联合类型
每个权限决策都携带 decisionReason,用于审计日志和用户提示:
type PermissionDecisionReason =
| { type: 'rule'; rule: PermissionRule } // 规则匹配
| { type: 'mode'; mode: PermissionMode } // 模式决定
| { type: 'safetyCheck'; reason: string; classifierApprovable: boolean } // 安全检查
| { type: 'workingDir'; reason: string } // 工作目录边界
| { type: 'classifier'; classifier: string; reason: string } // 分类器决策
| { type: 'hook'; hookName: string; reason?: string } // Hook 决策
| { type: 'subcommandResults'; reasons: Map<string, …> } // 子命令结果
| { type: 'permissionPromptTool'; permissionPromptToolName: string }
| { type: 'sandboxOverride' } // 沙箱覆盖
| { type: 'asyncAgent'; reason: string } // 无头代理
| { type: 'other'; reason: string } // 其他
12.2 权限请求消息生成
createPermissionRequestMessage 根据决策原因生成用户可读的消息:
switch (decisionReason.type) {
case 'classifier':
return `Classifier '${decisionReason.classifier}' requires approval: ${decisionReason.reason}`
case 'hook':
return `Hook '${decisionReason.hookName}' blocked this action: ${decisionReason.reason}`
case 'rule':
return `Permission rule '${ruleString}' from ${sourceString} requires approval`
case 'subcommandResults':
return `The following ${n} part(s) require approval: ${needsApproval.join(', ')}`
case 'safetyCheck':
case 'other':
return decisionReason.reason
case 'mode':
return `Current permission mode (${modeTitle}) requires approval`
}
13. 原书对照验证
| 1 | Fail-Closed 默认值:isReadonly: () => false | TOOL_DEFAULTS.isReadonly = () => false(4.1-4.2 笔记已确认) | ✅ 一致 |
| 2 | checkPermissions: allow(默认需权限检查) | TOOL_DEFAULTS.checkPermissions 返回 passthrough,最终转为 ask | ✅ 一致 |
| 3 | 工具描述即 Prompt,软硬结合控制 | description 字段 + checkPermissions 硬编码检查 | ✅ 一致 |
| 4 | 九步执行管线中步骤 6 权限决策 | hasPermissionsToUseTool 七步规则检查 + 模式后处理 | ✅ 一致 |
| 5 | 不对称代价:误判"安全">误判"危险" | deny 规则优先于 allow,安全检查 bypass 免疫 | ✅ 一致 |
| 6 | 工具默认不可用,必须显式注册 | 4.1-4.2 笔记已确认 Fail-Closed 注册机制 | ✅ 一致 |
| 7 | 工具基类统一 inputSchema/execute/validate 三段式 | Tool 接口的 inputSchema/call/checkPermissions | ✅ 一致 |
源码独有细节(原书未提及)
| 1 | bypass 免疫安全检查 | permissions.ts 步骤 1g | .git/、.claude/ 等敏感路径在 bypassPermissions 模式下仍提示 |
| 2 | bypass 免疫内容 ask 规则 | permissions.ts 步骤 1f | Bash(npm publish:*) 等 ask 规则在 bypass 模式下仍生效 |
| 3 | 影子规则检测 | shadowedRuleDetection.ts | 检测不可达的 allow 规则,防止配置错误导致安全假象 |
| 4 | 拒绝追踪状态机 | denialTracking.ts | 3 次连续/20 次总拒绝后回退到交互提示,防止无限拒绝循环 |
| 5 | Iron Gate 熔断器 | permissions.ts | 分类器不可用时 fail closed,30 分钟刷新的可配置门控 |
| 6 | Bundled Skills Nonce | filesystem.ts getBundledSkillsRoot | 每进程随机 16 字节 nonce 防止 /tmp 符号链接攻击 |
| 7 | 规则转义处理 | permissionRuleParser.ts | 括号转义防止规则内容注入解析器 |
| 8 | 遗留工具名归一化 | permissionRuleParser.ts | Task→Agent 等别名映射保证向后兼容 |
| 9 | 大小写不敏感路径比较 | filesystem.ts normalizeCaseForComparison | 防止 .cLauDe/Settings.locaL.json 绕过 |
| 10 | macOS 符号链接归一化 | filesystem.ts pathInWorkingPath | /var→/private/var、/tmp→/private/tmp 归一化 |
| 11 | 沙箱 auto-allow 例外 | permissions.ts 步骤 1b | Bash + 沙箱启用时,ask 规则不触发,沙箱自动放行 |
| 12 | Plan 模式权限优先 | spawnMultiAgent.ts buildInheritedCliFlags | planModeRequired 不继承 bypass 权限 |
| 13 | classifierApprovable 区分 | permissions.ts auto 模式 | Windows 路径模式不可分类(必须人工),危险文件可分类 |
| 14 | curl PR 检测 | gitOperationTracking.ts | 检测 curl -X POST + PR endpoint 的 REST API PR 创建 |
| 15 | .claude/ 会话级旁路 | filesystem.ts 步骤 1.6 | 会话级 allow 规则可绕过 .claude/ 安全检查(但仅限会话级) |
14. 设计模式提炼
14.1 Fail-Closed 默认值(原书 4.7.1)
所有默认值选择保守路径:
- isReadonly: () => false — 默认需要权限
- isConcurrencySafe: () => false — 默认串行执行
- checkPermissions 默认返回 passthrough → 转为 ask
- deny 规则优先于 allow 规则
- 安全检查 bypass 免疫
14.2 纵深防御
权限决策不是单一检查,而是多层瀑布:
deny 规则 → ask 规则 → 工具自定义检查 → 安全检查 → bypass 放行 → allow 规则 → 默认 ask
每一层独立拦截,前层通过不代表后层放行。
14.3 不对称剥离策略
与 BashTool 的 deny/allow 不对称剥离一致,文件系统权限也采用不对称策略:
- deny 检查:同时检查原始路径和符号链接解析路径(pathsToCheck 遍历)
- allow 检查:同样检查两个路径变体,但 classifierApprovable 区分可否自动审批
14.4 审计可追溯
每个权限决策携带 PermissionDecisionReason 联合类型,精确记录决策路径。createPermissionRequestMessage 将原因转换为用户可读消息,支持 10 种决策原因类型。
14.5 配置防呆
- 影子规则检测 — 发现不可达的 allow 规则
- 危险模式过滤 — auto 模式进入时剥离宽泛 allow 规则
- 规则转义 — 防止括号注入解析器
- 大小写归一化 — 防止混合大小写绕过
15. 工具协作关系图
┌─────────────────────────┐
│ hasPermissionsToUseTool │
│ (permissions.ts) │
└────────────┬─────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ BashTool │ │ FileEditTool │ │ ReadTool │
│ checkPerms │ │ checkPerms │ │ checkPerms │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ ┌───────────┴───────────┐ │
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────────┐
│ filesystem.ts (共享权限引擎) │
│ checkWritePermissionForTool / │
│ checkReadPermissionForTool / │
│ checkPathSafetyForAutoEdit / │
│ matchingRuleForInput / │
│ pathInAllowedWorkingPath │
└────────────────────┬───────────────────────────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│Permission │ │ dangerous │ │ denial │
│ Rule.ts │ │ Patterns │ │ Tracking │
│ (规则定义) │ │ (危险模式) │ │ (拒绝追踪) │
└───────────┘ └───────────┘ └───────────┘
16. 总结
tools/shared/ 目录本身的两个文件(gitOperationTracking.ts 和 spawnMultiAgent.ts)分别负责 Git 操作追踪和多代理生成,但跨工具共享的安全逻辑主要分布在 utils/permissions/ 目录下,构成所有工具权限检查的公共底座:
这些共享逻辑确保了 BashTool、FileEditTool、FileReadTool、FileWriteTool 等所有工具遵循一致的权限检查标准,是 Claude Code 安全体系的公共基础设施。


