# 第二阶段 · 模块二 · 第二节:内置核心工具
Read、Write、Edit、Bash 等核心工具是如何实现的?Bash 工具的安全机制是怎样的?工具参数验证的流程是什么?
Claude Code 全局架构
┌─────────────────────────────────────────────────────────────────────┐
│ 入口层 → 查询引擎层 → 工具/服务层 │
│ │
│ tools.ts (40+工具注册) │
│ ├── FileReadTool ← 本节 │
│ ├── FileWriteTool │
│ ├── FileEditTool │
│ ├── BashTool │
│ ├── WebSearchTool │
│ └── ... │
└─────────────────────────────────────────────────────────────────────┘
源码位置:`src/Tool.ts`
export interface Tool {
name: string;
description: string;
// 构建 MCP 格式的工具定义
buildTool(): ToolRecord;
// 执行工具
execute(
args: ToolArgs,
context: ToolContext,
): Promise<ToolResult>;
}
源码位置:`src/Tool.ts`
// 工具必须实现的接口
interface ToolRecord {
name: string;
description: string;
input_schema: z.ZodSchema; // 参数验证
}
// 工具执行上下文
interface ToolContext {
toolUseId: string;
toolUseContext: ToolUseContext;
messages: Message[];
// ...
}
源码位置:`src/tools/FileReadTool/FileReadTool.ts`
export class Read {
name = 'Read';
description = 'Read a file from the filesystem';
buildTool(): ToolRecord {
return {
name: 'Read',
description: 'Read a file from the filesystem',
input_schema: z.object({
file_path: z.string().describe('Path to the file to read'),
offset: z.number().optional().describe('Line number to start reading from'),
limit: z.number().optional().describe('Maximum number of lines to read'),
}),
};
}
async execute(args: { file_path: string; offset?: number; limit?: number }) {
// 1. 验证路径
const validatedPath = await validatePath(args.file_path);
// 2. 读取文件
const content = await readFileContent(validatedPath, {
offset: args.offset,
limit: args.limit,
});
// 3. 返回结果
return {
content,
citations: extractCitations(content),
};
}
}
问 1:为什么需要 offset 和 limit?
// offset: 从指定行开始读取
// limit: 限制读取的行数
// 用途:
// 1. 避免读取过大的文件
// 2. 支持分页读取
// 3. 减少 token 消耗
问 2:路径验证的流程是什么?
async function validatePath(path: string): Promise<string> {
// 1. 展开路径(~、环境变量等)
const expandedPath = expandPath(path);
// 2. 解析为绝对路径
const absolutePath = resolvePath(expandedPath);
// 3. 安全检查(禁止访问 /etc, /sys 等)
if (isForbiddenPath(absolutePath)) {
throw new SecurityError('Cannot read forbidden path');
}
// 4. 返回规范化路径
return absolutePath;
}
问 3:如何处理大文件?
// 如果文件超过限制,自动截断
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
async function readFileContent(path: string, options: { offset?: number; limit?: number }) {
const stats = await fs.stat(path);
if (stats.size > MAX_FILE_SIZE) {
// 返回部分内容 + 警告
const content = await readPartialContent(path, options);
return {
content,
truncated: true,
warning: `File truncated at ${MAX_FILE_SIZE} bytes`,
};
}
}
问 4:citations 是什么?
// citations 用于追踪文件内容的来源
// 当模型引用文件内容时,可以追踪到具体文件和行号
function extractCitations(content: string): Citation[] {
// 分析内容,提取引用信息
return [];
}
问 5:Read 工具在什么权限模式下可用?
// Read 是低风险工具,通常在所有权限模式下可用
// 但某些严格模式可能会限制访问特定目录
const replSimple = [
new Read({ ... }), // 始终可用
new Glob({ ... }), // 始终可用
new Grep({ ... }), // 始终可用
new Bash({ shell: '/bin/bash' }), // 可能受限
];
源码位置:`src/tools/FileWriteTool/FileWriteTool.ts`
export class Write {
name = 'Write';
description = 'Write content to a file';
buildTool(): ToolRecord {
return {
name: 'Write',
description: 'Write content to a file, creating or overwriting',
input_schema: z.object({
file_path: z.string().describe('Path to the file to write'),
content: z.string().describe('Content to write'),
}),
};
}
async execute(args: { file_path: string; content: string }) {
// 1. 验证路径
const validatedPath = await validateWritePath(args.file_path);
// 2. 写入文件
await writeTextContent(validatedPath, args.content);
// 3. 返回结果
return {
success: true,
path: validatedPath,
};
}
}
问 1:Write 和 Edit 的区别?
| 方面 | Write | Edit |
| 操作 | 覆盖整个文件 | 只修改部分内容 |
| 风险 | 高(可能丢失内容) | 低(只改一部分) |
| 用途 | 创建新文件 | 修改现有文件 |
问 2:如何防止意外覆盖?
// 在写入前检查文件是否存在
async function safeWrite(path: string, content: string) {
const exists = await pathExists(path);
if (exists) {
// 文件存在,提示用户或创建备份
await createBackup(path);
}
await writeTextContent(path, content);
}
问 3:Write 需要哪些权限?
// Write 是中等风险操作
// 需要在权限检查中明确允许
const simpleTools = [
// ...
new Write({ ... }), // 通常需要 'write' 权限
];
问 4:写入失败怎么办?
async function execute(args) {
try {
await writeTextContent(args.file_path, args.content);
} catch (error) {
if (error.code === 'EACCES') {
throw new ToolError('Permission denied: cannot write to this path');
}
if (error.code === 'ENOSPC') {
throw new ToolError('No space left on device');
}
throw error;
}
}
问 5:如何处理二进制文件?
// Write 工具主要用于文本文件
// 二进制文件通常需要特殊处理
const MAX_TEXT_SIZE = 10 * 1024 * 1024; // 10MB
async function execute(args) {
if (isBinaryContent(args.content)) {
throw new ToolError('Binary content not supported. Use Write for text files only.');
}
if (args.content.length > MAX_TEXT_SIZE) {
throw new ToolError(`Content too large: ${args.content.length} bytes (max: ${MAX_TEXT_SIZE})`);
}
}
源码位置:`src/tools/FileEditTool/FileEditTool.ts`
export class Edit {
name = 'Edit';
description = 'Make a small, targeted edit to a file';
buildTool(): ToolRecord {
return {
name: 'Edit',
description: 'Make a small, targeted edit to a file',
input_schema: z.object({
file_path: z.string().describe('The file to edit'),
old_string: z.string().describe('The text to replace'),
new_string: z.string().describe('The replacement text'),
}),
};
}
async execute(args: { file_path: string; old_string: string; new_string: string }) {
// 1. 验证 old_string 存在
const content = await readFileContent(args.file_path);
if (!content.includes(args.old_string)) {
throw new ToolError(
`old_string not found in file.\n` +
`File contents:\n${content.slice(0, 500)}...`
);
}
// 2. 替换
const newContent = content.replace(args.old_string, args.new_string);
// 3. 写回文件
await writeTextContent(args.file_path, newContent);
return { success: true };
}
}
问 1:为什么需要 old_string 和 new_string?
// old_string 用于精确定位要修改的内容
// 而不是简单的行号或偏移量
// 这样可以避免并发编辑的冲突问题
// 即使文件在其他地方被修改,只要 old_string 匹配就安全
问 2:old_string 不存在会怎样?
// 返回详细的错误信息
throw new ToolError(
`old_string not found in file.\n` +
`File contents:\n${content.slice(0, 500)}...`
);
// 帮助模型理解问题并修复
问 3:Edit 和 Write 的安全对比?
| 方面 | Write | Edit |
| 覆盖风险 | 高 | 低 |
| 精确性 | 低 | 高 |
| 并发安全 | 低 | 高 |
| 适用场景 | 创建文件 | 修改文件 |
问 4:如何支持多处的替换?
// 如果需要替换多处,使用 replaceAll
const newContent = content.replaceAll(args.old_string, args.new_string);
问 5:Edit 支持 sed 语法吗?
源码位置:`src/tools/BashTool/sedEditParser.ts`
// Claude Code 可能支持类似 sed 的语法
// 但主要是通过 Edit 工具的 old_string/new_string 机制
// sed 示例:s/foo/bar/g
// Edit 等价:
edit({
file_path: 'example.txt',
old_string: 'foo',
new_string: 'bar',
});
源码位置:`src/tools/BashTool/BashTool.tsx`(1143行)
export class BashTool {
name = 'Bash';
description = 'Execute a bash command';
buildTool(): ToolRecord {
return {
name: 'Bash',
description: 'Execute a bash command in the terminal',
input_schema: z.object({
command: z.string().describe('The bash command to execute'),
timeout_ms: z.number().optional().describe('Command timeout in milliseconds'),
}),
};
}
async execute(args: { command: string; timeout_ms?: number }) {
// 1. 解析命令
const parsedCommand = parseCommand(args.command);
// 2. 安全检查
const securityResult = await securityCheck(parsedCommand);
if (!securityResult.allowed) {
throw new ToolError(`Security check failed: ${securityResult.reason}`);
}
// 3. 执行命令
const result = await exec(parsedCommand.command, {
timeout: args.timeout_ms,
cwd: this.cwd,
});
return formatResult(result);
}
}
源码位置:`src/tools/BashTool/bashSecurity.ts`
// 安全检查流程
interface SecurityCheckResult {
allowed: boolean;
reason?: string;
riskLevel?: 'low' | 'medium' | 'high';
}
async function securityCheck(command: ParsedCommand): Promise<SecurityCheckResult> {
// 1. 检查危险命令模式
if (matchesDangerousPattern(command.raw)) {
return { allowed: false, reason: 'Command matches dangerous pattern', riskLevel: 'high' };
}
// 2. 检查只读约束
if (violatesReadOnlyConstraints(command.raw)) {
return { allowed: false, reason: 'Command violates read-only constraints', riskLevel: 'high' };
}
// 3. 检查路径遍历
if (containsPathTraversal(command.raw)) {
return { allowed: false, reason: 'Path traversal detected', riskLevel: 'high' };
}
// 4. 检查网络操作
if (containsNetworkOperation(command.raw)) {
return { allowed: false, reason: 'Network operations not allowed', riskLevel: 'medium' };
}
return { allowed: true, riskLevel: assessRisk(command) };
}
问 1:有哪些危险命令被禁止?
// 危险命令模式
const DANGEROUS_PATTERNS = [
/^rm\s+-rf\s+/, // rm -rf /
/^dd\s+/, // dd 命令
/^mkfs/, // 格式化
/^mount\s+.*bind/, // bind mount
/>/dev\/s/, // 直接写入设备
/:\(\)\{.*:\|:&};:/, // fork bomb
];
问 2:什么是只读约束?
// 只读模式下只能执行特定命令
const READ_ONLY_CONSTRAINTS = {
allowed: [
'ls', 'cat', 'head', 'tail', 'grep', 'find', 'which',
'pwd', 'cd', 'echo', 'true', 'false',
],
denied: [
'rm', 'mv', 'cp', 'chmod', 'chown', 'dd',
],
};
问 3:Bash 如何处理超时?
// 默认超时
const DEFAULT_TIMEOUT_MS = 60_000; // 60秒
// 可以通过参数覆盖
const result = await exec(command, {
timeout: args.timeout_ms ?? DEFAULT_TIMEOUT_MS,
});
// 超时处理
if (result.timedOut) {
throw new ToolError(`Command timed out after ${timeout}ms`);
}
问 4:什么是 sandbox 模式?
源码位置:`src/tools/BashTool/shouldUseSandbox.ts`
// Sandbox 模式限制命令的权限
const shouldUseSandbox = (command: string): boolean => {
// 危险命令使用 sandbox
if (matchesDangerousPattern(command)) {
return true;
}
// 实验性功能:AI 决定是否使用 sandbox
if (feature('AI_SANDBOX_SELECTION')) {
return await aiRecommendsSandbox(command);
}
return false;
};
问 5:Bash 工具的权限级别?
| 权限级别 | 可用命令 | 风险 |
| minimal | ls, cat, grep, cd | 低 |
| low | + find, which, echo | 低 |
| medium | + git, npm, node | 中 |
| high | + rm, mv, chmod | 高 |
| all | 所有命令 | 极高 |
源码位置:`src/Tool.ts`
import { z } from 'zod/v4';
// 每个工具通过 Zod schema 定义输入参数
const WriteSchema = z.object({
file_path: z.string().describe('Path to the file to write'),
content: z.string().describe('Content to write'),
});
interface ValidationResult {
success: boolean;
error?: string;
parsed?: z.infer<typeof WriteSchema>;
}
function validateInput(schema: z.ZodSchema, input: unknown): ValidationResult {
const result = schema.safeParse(input);
if (!result.success) {
return {
success: false,
error: result.error.message,
};
}
return {
success: true,
parsed: result.data,
};
}
问 1:为什么使用 Zod?
// Zod 提供运行时验证 + 类型推断
// 1. 类型安全
const parsed = WriteSchema.parse(input);
// parsed 类型是 { file_path: string; content: string }
// 2. 验证消息
const result = WriteSchema.safeParse(input);
if (!result.success) {
console.log(result.error.message); // 人类可读的验证错误
}
问 2:参数验证的流程?
async function execute(tool: Tool, rawInput: unknown) {
// Step 1: Schema 验证
const validation = validateInput(tool.input_schema, rawInput);
if (!validation.success) {
throw new ValidationError(validation.error);
}
// Step 2: 业务逻辑验证
if (!await pathExists(validation.parsed.file_path)) {
throw new ToolError('File does not exist');
}
// Step 3: 执行
return await tool.execute(validation.parsed);
}
问 3:如何自定义验证错误消息?
const WriteSchema = z.object({
file_path: z.string().min(1, 'Path cannot be empty'),
content: z.string().refine(
content => !content.includes('\x00'), // 不能包含 null 字节
{ message: 'Content cannot contain null bytes' }
),
});
问题:如果需要自定义工具的错误处理逻辑(如重试、降级),如何实现?
答案:
class RetryableTool implements Tool {
constructor(
private inner: Tool,
private maxRetries: number = 3,
) {}
async execute(args: ToolArgs): Promise<ToolResult> {
for (let i = 0; i < this.maxRetries; i++) {
try {
return await this.inner.execute(args);
} catch (error) {
if (i === this.maxRetries - 1) throw error;
// 指数退避
await sleep(Math.pow(2, i) * 1000);
}
}
throw new Error('Unreachable');
}
}
问题:对于长时间运行的命令(如 `tail -f`),能否支持流式输出,而不是等待完成?
答案:
// 可以通过 AsyncGenerator 实现流式输出
async *execute(args: { command: string }): AsyncGenerator<string> {
const process = spawn('bash', ['-c', args.command]);
for await (const line of process.stdout) {
yield line; // 边读边 yield
}
}
// 使用方
for await (const line of tool.execute({ command: 'tail -f log.txt' })) {
console.log(line);
}
问题:如果需要在多个工具调用之间共享状态(如缓存、临时文件),如何实现?
答案:
// 通过 ToolContext 传递共享状态
interface SharedState {
cache: Map<string, unknown>;
tempFiles: string[];
}
interface ToolContext {
toolUseId: string;
sharedState: SharedState; // 工具之间共享
}
// 示例:WriteTool 创建临时文件,ReadTool 读取
async function execute(args, context) {
if (args.is_temp) {
context.sharedState.tempFiles.push(args.file_path);
}
}
| 文件 | 行数 | 核心内容 |
| `src/tools/FileReadTool/FileReadTool.ts` | ? | 文件读取 |
| `src/tools/FileWriteTool/FileWriteTool.ts` | ? | 文件写入 |
| `src/tools/FileEditTool/FileEditTool.ts` | ? | 文件编辑 |
| `src/tools/BashTool/BashTool.tsx` | 1143 | Bash 命令执行 |
下一节我们将深入 工具执行机制:
- StreamingToolExecutor 的并行执行
- 工具结果如何返回给模型
- 工具执行失败的处理
*- 第一轮:□ 事实准确性*
*- 第二轮:□ 深度与洞见*
*- 第三轮:□ 可读性与价值*