Topic | BashTool 安全模型:16 个文件全拆¶
重要性:⭐⭐⭐⭐⭐(LLM Agent 最危险的能力 = 跑 shell) 出现位置:
src/tools/BashTool/(16 文件)、src/utils/bash/(bashParser 4436 行 + ast 2679 行) 关联:phase-05-tools.md 的 BashTool 16 文件拆解、glossary 的 Sandbox 词条
1. 为什么 BashTool 拆这么细¶
BashTool 是 Claude Code 最危险的能力:
- LLM 可以执行任意 shell 命令
- 一句 rm -rf / 就能毁掉用户机器
- 必须有纵深防御
纵深防御(Defense in Depth) 的 4 层:
┌────────────────────────────────────────┐
│ Layer 4: 沙箱(最外层) │
│ shouldUseSandbox.ts → macOS Seatbelt │
│ 即使命令在跑,也只能动限定文件 │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ Layer 3: 危险命令检测(实时) │
│ destructiveCommandWarning.ts │
│ bashSecurity.ts │
│ 检测 rm -rf /, dd, mkfs 等 │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ Layer 2: 路径 + 权限(静态分析) │
│ pathValidation.ts │
│ bashPermissions.ts │
│ shellRuleMatching.ts │
│ 通配符规则匹配 + 路径白名单 │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ Layer 1: 命令解析(最内层) │
│ bashCommandHelpers.ts │
│ commandSemantics.ts │
│ bashParser.ts (4436 行!) │
│ ast.ts (2679 行!) │
│ Tree-sitter 解析 + 语义分析 │
└────────────────────────────────────────┘
2. 16 个文件总览¶
src/tools/BashTool/
├── BashTool.tsx 主实现(推测)
├── UI.tsx 渲染(含危险警告 UI)
├── prompt.ts LLM 看的工具描述
├── utils.ts 杂项
├── toolName.ts 工具名定义
│
│ ── 命令解析层(Layer 1) ──
├── bashCommandHelpers.ts 命令解析辅助
├── commandSemantics.ts 命令语义(cd / export / 副作用)
├── commentLabel.ts 命令注释提取
│
│ ── 路径 + 权限层(Layer 2) ──
├── pathValidation.ts 路径校验
├── bashPermissions.ts bash 权限规则(2621 行)
├── readOnlyValidation.ts 只读模式校验
├── modeValidation.ts BashMode 校验
│
│ ── 危险命令检测层(Layer 3) ──
├── bashSecurity.ts 安全检查(2592 行)
├── destructiveCommandWarning.ts 破坏性命令警告
│
│ ── 沙箱层(Layer 4) ──
├── shouldUseSandbox.ts 是否走沙箱
│
│ ── sed 解析(特殊) ──
├── sedEditParser.ts sed 命令解析
└── sedValidation.ts sed 安全性
3. Layer 1:命令解析¶
3.1 bashParser.ts (4436 行) —— Tree-sitter bash 解析¶
// src/utils/bash/bashParser.ts 推测
import Parser from 'tree-sitter-bash'
const parser = new Parser()
parser.setLanguage(bash)
export function parseBash(command: string): AST {
return parser.parse(command).rootNode
}
为什么用 Tree-sitter:
- bash 语法复杂(if/while/for/case/function 嵌套)
- 正则匹配不够(a && b || c 和 a && (b || c) 是不同 AST)
- Tree-sitter 是工业级 parser,能正确处理嵌套
3.2 ast.ts (2679 行) —— AST 工具¶
// src/utils/bash/ast.ts 推测
export function isDangerous(ast: AST): boolean {
// 1. 遍历 AST 找敏感命令
// 2. 检测路径越界
// 3. 检测命令链(a && b && c)
}
export function extractSubcommands(ast: AST): string[] {
// 拆出每条子命令
return ast.descendantsOfType('command').map(c => c.text)
}
AST 工具的常见操作:
- walk(ast, visitor) —— 深度优先遍历
- findByType(ast, 'redirect') —— 找重定向
- getCommandName(node) —— 提取命令名
3.3 commandSemantics.ts —— 语义分析¶
// src/tools/BashTool/commandSemantics.ts 推测
export function classifyCommand(cmd: string): CommandSemantics {
// 1. 是否有副作用?(mkdir > ls)
// 2. 是否改变工作目录?(cd)
// 3. 是否修改环境变量?(export)
// 4. 是否读 / 写文件?(cat > echo)
return {
hasSideEffects: ...,
changesCwd: ...,
modifiesEnv: ...,
readsFiles: ...,
writesFiles: ...,
}
}
意义: - 决定"该走沙箱吗" - 决定"该不该询问用户" - 决定"是否需要保存工作目录"
4. Layer 2:路径 + 权限¶
4.1 pathValidation.ts¶
// src/tools/BashTool/pathValidation.ts 推测
export function validatePath(path: string, ctx: ToolUseContext): ValidationResult {
// 1. 绝对路径 → 检查是否在 allowedDirectories
// 2. 相对路径 → 解析为绝对,检查 CWD
// 3. 包含 .. → 拒绝(防越权)
// 4. 符号链接 → 解析为真实路径再检查
}
典型规则:
- 拒绝 /etc、/usr、/var 写入
- 拒绝 .. 越权
- 强制 sandbox 内只能写 ~/.claude/sandbox/
4.2 bashPermissions.ts (2621 行) —— 通配符规则¶
// src/tools/BashTool/bashPermissions.ts
export type BashPermissionRule = {
pattern: string // 'Bash(npm install:*)'
decision: 'allow' | 'deny' | 'ask'
source: 'user' | 'project' | 'policy'
}
// 用户在 settings.json 里写:
{
"permissions": {
"allow": ["Bash(npm install:*)", "Bash(git status)"],
"deny": ["Bash(rm -rf /*)"]
}
}
规则匹配逻辑:
// shellRuleMatching.ts
export function matchWildcardPattern(pattern: string, command: string): boolean {
// 'Bash(npm install:*)' 匹配 'npm install foo bar'
// 'Bash(rm -rf /*)' 匹配 'rm -rf /etc'
}
2621 行的实现复杂度:
- 通配符语法(* vs ? vs [abc])
- 多规则优先级
- 拒绝规则 override 允许规则
- 来源优先级(policy > project > user)
4.3 readOnlyValidation.ts + modeValidation.ts¶
// readOnlyValidation.ts
export function isReadOnlyCommand(cmd: string): boolean {
// 检测是否是只读命令
// ls, cat, grep, find (无 -delete), ...
// → readOnly 模式可以自动放行
}
// modeValidation.ts
export function validateMode(cmd: string, mode: 'default' | 'acceptEdits' | 'plan'): boolean {
// 某些命令在 plan 模式禁用
// 某些命令在 acceptEdits 模式自动通过
}
5. Layer 3:危险命令检测¶
5.1 bashSecurity.ts (2592 行) —— 主安全检查¶
// src/tools/BashTool/bashSecurity.ts
export function checkCommandSecurity(
command: string,
ctx: SecurityContext
): SecurityCheck {
// 1. 解析 AST
const ast = parseBash(command)
// 2. 检测危险命令
const dangerous = DANGEROUS_COMMANDS.find(rule =>
rule.matches(ast)
)
if (dangerous) {
return { safe: false, reason: dangerous.reason, severity: dangerous.severity }
}
// 3. 检测敏感路径
const sensitivePath = SENSITIVE_PATHS.find(p =>
command.includes(p)
)
if (sensitivePath) {
return { safe: false, reason: 'Touches sensitive path', path: sensitivePath }
}
// 4. 启发式检查
if (looksLikeExfiltration(command)) {
return { safe: false, reason: 'Possible data exfiltration' }
}
return { safe: true }
}
const DANGEROUS_COMMANDS = [
{ pattern: /^rm\s+-rf\s+\/$/, reason: 'rm -rf /' },
{ pattern: /^dd\s+.*of=\/dev\//, reason: 'dd to device' },
{ pattern: /^mkfs/, reason: 'format filesystem' },
{ pattern: /:(){\s*:\|:&\s*};:/, reason: 'fork bomb' },
// ... 数千条
]
2592 行的复杂度: - 数千条危险命令规则 - 每条规则的描述、严重程度、降级方案 - 持续维护的"黑名单"
5.2 destructiveCommandWarning.ts —— 用户警告¶
// src/tools/BashTool/destructiveCommandWarning.ts
export function getDestructiveWarning(command: string): Warning | null {
// 检测"破坏性"命令,生成用户警告
if (/^rm\s+.*-[a-zA-Z]*r/.test(command)) {
return {
severity: 'high',
message: 'This will delete files. Are you sure?',
preview: 'Files that will be deleted: ...', // 解析命令 + 模拟执行
}
}
}
UI 集成:警告通过 PermissionRequest 弹给用户。
6. Layer 4:沙箱¶
6.1 shouldUseSandbox.ts¶
// src/tools/BashTool/shouldUseSandbox.ts
export function shouldUseSandbox(
command: string,
ctx: ToolUseContext
): boolean {
// 1. 用户没启用 sandbox? → false
if (!ctx.settings.sandboxEnabled) return false
// 2. 只读命令? → 不需要 sandbox
if (isReadOnlyCommand(command)) return false
// 3. 否则 → true
return true
}
6.2 实际沙箱实现(推测)¶
macOS:用 sandbox-exec + Seatbelt profile
profile.sb 内容:
(version 1)
(deny default)
(allow file-read* file-write* (subpath "/Users/me/project"))
(deny file-write* (subpath "/etc"))
(deny network-out)
Linux:用 bubblewrap (bwrap)
7. sed 特殊处理¶
7.1 sedEditParser.ts¶
// sed 's/old/new/g' file
export function parseSedEdit(command: string): SedEdit | null {
const match = command.match(/^sed\s+(.+)\s+(\S+)$/)
if (!match) return null
return {
expression: match[1], // 's/old/new/g'
target: match[2], // 'file.txt'
}
}
7.2 sedValidation.ts¶
export function validateSedEdit(edit: SedEdit): ValidationResult {
// 1. 检测 's/.*//g' 这种"全删"
// 2. 检测 '/d'(删除行)
// 3. 检测 '/w file'(写到其他文件)
}
为什么 sed 特殊:sed 的 -i 原地编辑不可逆(不像 git 那样有历史)。
8. UI.tsx 用户看到什么¶
// src/tools/BashTool/UI.tsx 推测
function BashToolUI({ input, requiresApproval, securityCheck }) {
return (
<Box flexDirection="column">
<Text color="cyan">$ {input.command}</Text>
{securityCheck && !securityCheck.safe && (
<Box borderStyle="round" borderColor="red">
<Text color="red">⚠ {securityCheck.reason}</Text>
</Box>
)}
<Box flexDirection="row" marginTop={1}>
<Text dimColor>Working directory: {input.cwd}</Text>
{input.timeout && <Text dimColor>Timeout: {input.timeout}ms</Text>}
{input.sandbox && <Text color="green">[sandboxed]</Text>}
</Box>
<KeyboardShortcutHint keys="Enter" label="Approve" />
<KeyboardShortcutHint keys="Esc" label="Deny" />
</Box>
)
}
9. 实战:一条命令的完整路径¶
用户: "删除 /tmp/old.log"
↓
LLM 返回 tool_use: { name: 'Bash', input: { command: 'rm /tmp/old.log' } }
↓
Layer 1: parseBash('rm /tmp/old.log') → AST
classifyCommand → hasSideEffects=true, writesFiles=true
↓
Layer 2: pathValidation('/tmp/old.log') → ok(在 allowedDirectories)
bashPermissions 匹配 → 没规则
↓
Layer 3: bashSecurity 检查 → 没命中危险规则
destructiveCommandWarning → severity: medium(rm 但非 -rf)
↓
Layer 4: shouldUseSandbox → true(要写文件)
↓
PermissionRequest 弹给用户
↓
用户批准 → spawn('rm /tmp/old.log', { sandbox: ... })
↓
实时 yield stdout(如果有)
↓
exit code 0 → ToolResult(content: 'Command completed')
↓
注入回 messages
10. 关键洞察¶
10.1 纵深防御不可省¶
单层防御永远会被绕过。Claude Code 用了 4 层: - 解析(理解命令是什么) - 路径(理解操作什么文件) - 危险(理解命令是否安全) - 沙箱(兜底隔离)
10.2 Tree-sitter 是行业标准¶
任何"理解代码"的需求,用 parser > 正则。bashParser 4436 行说明这不是简单任务。
10.3 危险命令黑名单是"持续维护"的¶
bashSecurity 2592 行的规则库。这不是一次写完的代码,是持续更新的"漏洞库"。
10.4 sed 特殊化的哲学¶
"危险"的判断是相对的:
- rm file 普通
- rm -rf / 致命
- sed -i 's/.*//g' file 等同 rm file
- 不能一刀切,要按语义分析
11. 阅读清单¶
- ✅
src/utils/bash/bashParser.ts:1-50(Tree-sitter 入口) - ✅
src/utils/bash/ast.ts:1-50(AST 工具) - ✅
src/tools/BashTool/commandSemantics.ts(语义分类) - ✅
src/tools/BashTool/bashPermissions.ts:1-80(权限规则) - ✅
src/tools/BashTool/bashSecurity.ts:1-80(危险检测) - ✅
src/tools/BashTool/destructiveCommandWarning.ts(用户警告) - ✅
src/tools/BashTool/shouldUseSandbox.ts(沙箱判断) - 📌
src/tools/BashTool/UI.tsx(用户看到的界面)
12. 练习任务¶
- 画 4 层防御的流程图 —— 一条
rm -rf /tmp/old.log经过每层做了什么 - 手写一个简单的 bash parser —— 不需要 Tree-sitter,能处理
cmd1 && cmd2 || cmd3即可 - 设计危险命令规则 —— 列 20 条你想到的危险命令,分类(删除/网络/权限提升...)
- 思考:为什么 Claude Code 不直接用
bubblewrap/sandbox-exec跑所有命令,省掉前 3 层?性能 vs 安全的 trade-off 是什么?