欢迎光临
我们一直在努力

4.5 共享安全逻辑 — 跨工具权限检查与参数验证的工程实现

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):

  • NTFS 交换数据流(ADS) — file.txt::$DATA、.bashrc:hidden(仅 Windows/WSL 检查冒号)
  • 8.3 短文件名 — GIT~1、CLAUDE~1、SETTIN~1.JSON
  • 长路径前缀 — \\\\?\\C:\\、\\\\.\\C:\\、//?/C:/、//./C:/
  • 尾部点和空格 — .git.、.claude 、.bashrc…(Windows 在路径解析时剥离)
  • DOS 设备名 — .git.CON、settings.json.PRN、.bashrc.AUX
  • 连续三点 — …/file.txt、path/…/file
  • UNC 路径 — \\\\server\\share、//foo.com/file(全平台检查)
  • 危险文件列表:

    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 的三重防御:

  • macOS 符号链接归一化 — /var → /private/var、/tmp → /private/tmp
  • 大小写不敏感比较 — normalizeCaseForComparison() 防止 .cLauDe/CoMmAnDs 绕过
  • 路径遍历检测 — containsPathTraversal(relative) 拒绝 ../ 模式
  • 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/ 目录下,构成所有工具权限检查的公共底座:

  • 权限模式系统 — 6 种模式从 plan(最安全)到 bypassPermissions(最危险),通过条件编译支持内部专用模式
  • 权限决策管线 — hasPermissionsToUseTool 七步规则检查 + 模式后处理,含 bypass 免疫的安全检查
  • 文件系统权限引擎 — checkWritePermissionForTool 八步流水线和 checkReadPermissionForTool 八步流水线,所有文件操作工具共享
  • 规则匹配引擎 — gitignore 语义的模式匹配,支持 4 种路径前缀和符号链接双重检查
  • 路径安全检查 — 7 种 Windows 路径攻击模式 + 10 个危险文件 + 4 个危险目录 + 大小写归一化
  • 拒绝追踪 — 3 次连续/20 次总拒绝后回退到交互提示,无头模式直接中止
  • 影子规则检测 — 发现被工具级 ask/deny 规则覆盖的不可达 allow 规则
  • 危险模式过滤 — auto 模式进入时剥离宽泛的代码执行 allow 规则
  • Git 操作追踪 — 跨 Shell 的正则检测,自动追踪 commit/push/PR 操作并链接会话
  • 多代理权限继承 — Plan 模式优先于 bypass,所有用户输入经 quote() 消毒
  • 这些共享逻辑确保了 BashTool、FileEditTool、FileReadTool、FileWriteTool 等所有工具遵循一致的权限检查标准,是 Claude Code 安全体系的公共基础设施。

    赞(0)
    未经允许不得转载:171主机测评 » 4.5 共享安全逻辑 — 跨工具权限检查与参数验证的工程实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址