Claude Code 源码解剖

Claude Code 的工具提示词

从 BashTool、FileEditTool、FileReadTool、GrepTool、AgentTool 和 SkillTool 的 prompt 源码,拆解工具描述如何成为局部行为协议。

第 8 章 16 分钟

Claude Code 的工具提示词

系统提示词管全局行为,工具提示词管局部动作。模型准备调用某个工具时,看到的不只是工具名和参数 schema,还会看到这个工具自己的使用协议:什么时候用、什么时候别用、参数怎么写、错误怎么恢复、有哪些安全禁区。

这一章不再讲工具注册和执行,而是专门读工具 prompt 源码,看看 Claude Code 如何把每个工具做成一个“微型控制面”。

核心直觉

工具提示词不是说明书,而是局部行为协议。它把“功能描述、正向引导、负面禁令、参数语义、恢复路径、资源预算”一起交给模型。

先看源码入口

源码定位点关键符号负责什么
restored-src/src/tools/BashTool/prompt.ts:275-369getSimplePrompt()Bash 工具主提示词:工具偏好、命令执行、sleep 反模式
restored-src/src/tools/BashTool/prompt.ts:42-161getCommitAndPRInstructions()Git 安全协议和 commit/PR 工作流
restored-src/src/tools/BashTool/prompt.ts:172-273getSimpleSandboxSection()沙箱策略 JSON 内联
restored-src/src/tools/FileEditTool/prompt.ts:1-28getPreReadInstruction()、old_string 规则编辑前必须读取、最小唯一匹配、行号前缀处理
restored-src/src/tools/FileReadTool/prompt.ts:1-49MAX_LINES_TO_READ、offset/limit 指令默认读取 2000 行、分段读取、多媒体能力声明
restored-src/src/tools/GrepTool/prompt.ts:1-18Grep 使用规则搜索必须用 Grep,避免 Bash grep/rg
restored-src/src/tools/GrepTool/GrepTool.ts:33-89input schema输出模式、head_limit 等结果控制
restored-src/src/tools/AgentTool/prompt.ts:1-287agent 列表、fork 指引委派任务质量、动态 agent 列表缓存优化
restored-src/src/tools/SkillTool/prompt.ts:1-242skill 列表预算和调用协议1% 上下文预算、三级截断、阻塞调用要求

读工具提示词要和运行时实现一起读。提示词负责提前引导模型,运行时代码负责真正检查。

总流程图

flowchart TB A["工具 description / prompt"] --> B["模型选择工具"] C["inputSchema"] --> D["模型生成参数"] B --> D D --> E["运行时校验和权限"] E --> F["tool.call()"] F --> G["tool_result 回写"] G --> H["下一轮模型修正工具使用"] A --> A1["正向引导<br/>应该用什么"] A --> A2["负向禁令<br/>不要怎么用"] A --> A3["条件分支<br/>何时用 offset / multiline / background"] A --> A4["格式模板<br/>old_string / prompt / skill listing"] A --> A5["资源预算<br/>head_limit / 1% budget / PDF pages"]

一个工具提示词通常同时回答六个问题:

问题例子
这个工具做什么Grep 搜索内容,Edit 替换文本
什么时候用搜索任务必须用 Grep
什么时候不用Bash 不要拿来 catgrepsed
参数怎么写Edit 的 old_string 必须唯一,Read 可用 offset/limit
出错怎么修搜索过多用 head_limit,编辑失败扩大上下文
结果如何节制Read 默认 2000 行,Skill 列表最多 1% 上下文

万能执行入口:BashTool 必须被导流

Bash 是最危险也最容易被滥用的工具。因为它几乎什么都能做,所以提示词首先要告诉模型:能用 Bash,不代表应该用 Bash。

工具偏好矩阵:把 Bash 流量导到专用工具

原文给出的 Bash 提示词片段:

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)',
];

这段代码证明 BashTool prompt 的第一任务不是教模型写 shell,而是做工具路由。

场景应用工具禁止倾向
文件查找Globfindls
内容搜索Grepgreprg
读文件Readcatheadtail
改文件Editsedawk
写文件Writeecho &gt;、HEREDOC
输出文本直接回复echoprintf

为什么要这么强?因为专用工具外面包了 schema、权限、结果预算和 UI 渲染。Bash 字符串没有这些结构化边界。

容易误解

“Bash 能做”不是工具选择标准。Claude Code 希望模型优先选择可审计、可校验、可预算的专用工具。

多命令并发:提示词直接给调度语义

BashTool 提示词还有多命令规则:

If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message.
If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.
Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
DO NOT use newlines to separate commands.

这段文本证明工具提示词可以承载调度策略:

情况推荐写法语义
独立命令多个 Bash tool calls交给工具编排并发
有依赖cmd1 &amp;&amp; cmd2前一个成功才执行后一个
顺序但允许失败cmd1; cmd2不关心前一个失败
换行分隔禁止避免模型生成不可控脚本块

这和第 4 章的并发调度是配套的:提示词让模型正确表达依赖关系,执行器再根据工具输入做调度。

版本控制安全协议:把风险场景写成具体禁令

BashTool 的 Git 安全协议:

Git Safety Protocol:
- NEVER update the git config
- NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) unless the user explicitly requests these actions
- NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it
- NEVER run force push to main/master, warn the user if they request it
- CRITICAL: Always create NEW commits rather than amending, unless the user explicitly requests a git amend.
- When staging files, prefer adding specific files by name rather than using "git add -A" or "git add ."
- NEVER commit changes unless the user explicitly asks you to

这段提示词证明好的工具 prompt 会把风险展开成具体操作,而不是只写“注意安全”。

禁令防什么
不改 git config污染用户环境
不跑 destructive git覆盖历史、丢失工作区
不跳过 hooks绕过质量和签名检查
不 force push main/master破坏共享主分支
默认新建 commit,不 amendhook 失败后 amend 可能改到上一个 commit
staging 指定文件避免把 .env、凭证等扫进去
不主动 commit避免 Agent 过度自治

核心直觉

Git 协议里的 NEVER 不是风格用词。它告诉模型这些操作默认不属于可自由推断的行动空间,除非用户显式授权。

沙箱 JSON:把机器策略直接交给模型

沙箱启用时,BashTool 会把文件系统策略内联为 JSON:

const filesystemConfig = {
  read: {
    denyOnly: dedup(fsReadConfig.denyOnly),
    allowWithinDeny: dedup(fsReadConfig.allowWithinDeny),
  },
  write: {
    allowOnly: normalizeAllowOnly(fsWriteConfig.allowOnly),
    denyWithinAllow: dedup(fsWriteConfig.denyWithinAllow),
  },
};

这段代码证明工具提示词不一定只写自然语言。安全策略需要精确时,JSON 更合适。

原文还提到两个细节:

  • dedup() 去重路径,减少重复 token。
  • normalizeAllowOnly() 把用户临时目录归一成 $TMPDIR,既省 token,也提升 prompt cache 稳定性。

模型看到沙箱策略后,可以在生成命令前避开不允许的路径,而不是等运行时拒绝后再修正。

等待反模式:把无效 sleep 压下去

BashTool 还专门抑制 sleep

Do not sleep between commands that can run immediately.
If your command is long running, use run_in_background.
Do not retry failing commands in a sleep loop - diagnose the root cause.
If waiting for a background task, do not poll.
If you must sleep, keep the duration short (1-5 seconds).

这证明提示词会针对模型常见坏习惯写“反模式抑制”。模型很容易用 sleep &amp;&amp; retry 模拟异步等待,但交互式 Agent 更需要诊断、后台执行或事件反馈。

文件编辑工具:编辑前必须先读取

FileEditTool 提示词短,但每一句都在维护编辑正确性。

前置读取是硬约束,不是建议

function getPreReadInstruction(): string {
  return `You must use your \`${FILE_READ_TOOL_NAME}\` tool at least once
  in the conversation before editing. This tool will error if you
  attempt an edit without reading the file.`;
}

这段代码证明“先读后改”是双层防线:

作用
prompt让模型提前知道不读会失败,避免浪费工具调用
runtime没有 Read 历史时 Edit 直接报错

这解决的是幻觉编辑:模型不能凭记忆或猜测修改文件。

old_string 必须唯一,但也不能太大

FileEditTool 要求:

The edit will FAIL if old_string is not unique in the file.
Either provide a larger string with more surrounding context to make it unique
or use replace_all to change every instance.

内部用户还有更细的 token 经济学提示:

Use the smallest old_string that's clearly unique - usually 2-4 adjacent lines is sufficient.
Avoid including 10+ lines of context when less uniquely identifies the target.

这证明 old_string 要在两个目标之间平衡:

目标太低会怎样太高会怎样
唯一性匹配多个位置,工具失败或误改过多上下文浪费 token,且更容易因无关变化匹配失败
精确性模型凭猜测替换编辑成本变高

读文件输出和编辑输入之间有接口契约

提示词还说明从 Read 输出复制文本时,要去掉行号前缀:

const prefixFormat = isCompactLinePrefixEnabled()
  ? 'line number + tab'
  : 'spaces + line number + arrow';

并要求:

Everything after that is the actual file content to match.
Never include any part of the line number prefix in the old_string or new_string.

这段代码证明工具提示词承担了工具间接口文档的角色。Read 为了 UI 和定位添加行号,但 Edit 的匹配必须基于真实文件内容。

文件读取工具:读取也要预算感知

FileReadTool 的提示词主要控制读取范围和运行时能力声明。

默认 2000 行不是随便写的

export const MAX_LINES_TO_READ = 2000;

提示词里写:

By default, it reads up to 2000 lines starting from the beginning of the file.

这证明 Read 默认不是“全文件无上限”。2000 行足以覆盖多数单文件理解,又避免一次读取吞掉太多上下文。

分段读取参数有两种引导语气

export const OFFSET_INSTRUCTION_DEFAULT =
  "You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters";

export const OFFSET_INSTRUCTION_TARGETED =
  'When you already know which part of the file you need, only read that part. This can be important for larger files.';

这段代码证明同一个参数在不同阶段有不同策略:

模式适合阶段引导
DEFAULT初次理解文件尽量读完整文件,避免漏上下文
TARGETED已知目标位置只读需要的部分,省 token

能力声明必须和运行时对齐

ReadTool 提示词会声明图片、PDF、Notebook 支持,但 PDF 段落受 isPDFSupported() 条件控制。大 PDF 还要求指定 pages,每次最多 20 页。

这说明工具 prompt 不能承诺运行时做不到的能力:

运行时支持 PDF -> 提示词声明 PDF 能力和页数限制
运行时不支持 PDF -> 不在提示词里提 PDF

容易误解

在 prompt 里写“可以读 PDF”,不会让运行时突然具备 PDF 解析能力。能力声明必须由代码条件控制。

搜索工具:搜索必须走受控入口

GrepTool 提示词短,但非常硬:

ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.

这段说明和 BashTool 的偏好矩阵形成双向闭环:

BashTool: 搜索不要用 Bash grep/rg
GrepTool: 搜索必须用 Grep

为什么同样底层可能是 ripgrep,还要用 GrepTool?原文指出 GrepTool 外面包了:

  • checkReadPermissionForTool
  • getFileReadIgnorePatterns
  • VCS_DIRECTORIES_TO_EXCLUDE
  • 结构化结果和预算

通过 Bash 直接跑 rg 会绕过这些边界。

搜索语法纠偏

GrepTool 提示词还提醒:

Supports full regex syntax.
Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping.
Multiline matching: use multiline: true.

这里不是科普正则,而是在减少模型生成错误搜索的概率。multiline: true 底层对应 -U --multiline-dotall,但提示词选择暴露模型需要知道的参数,而不是暴露命令行细节。

head_limit:安全默认值和逃生口

GrepTool schema 里有:

const DEFAULT_HEAD_LIMIT = 250;

描述中说明:

Defaults to 250 when unspecified.
Pass 0 for unlimited (use sparingly - large result sets waste context).

这证明 Grep 的默认不是“尽量多返回”。250 是上下文保护;0 是明确逃生口,但提示词提醒谨慎使用。

子代理工具:动态列表外移,委派也要有质量标准

AgentTool 的提示词复杂,因为它既要告诉模型有哪些 agent,又要教模型如何写好委派任务。

子代理列表:内联还是附件

源码里有开关:

export function shouldInjectAgentListInMessages(): boolean {
  if (isEnvTruthy(process.env.CLAUDE_CODE_AGENT_LIST_IN_MESSAGES)) return true;
  if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_AGENT_LIST_IN_MESSAGES))
    return false;
  return getFeatureValue_CACHED_MAY_BE_STALE('tengu_agent_list_attach', false);
}

这段代码证明 agent 列表有两种注入方式:

方式行为取舍
内联工具描述agent 列表直接写进 tool description模型一眼看到,但 schema 频繁变
附件消息tool description 保持静态,列表放 @@INLINE_0@@保护工具 schema prompt cache

原文提到动态 agent 列表曾占全局 cache_creation token 约 10.2%。所以把列表移到附件,是为了保护工具 schema 层缓存。

agent 行格式:

export function formatAgentLine(agent: AgentDefinition): string {
  const toolsDescription = getToolsDescription(agent);
  return `- ${agent.agentType}: ${agent.whenToUse} (Tools: ${toolsDescription})`;
}

这说明模型不只需要知道 agent 类型,还要知道这个 agent 能用哪些工具。委派边界也是工具提示词的一部分。

分叉子代理:不要偷看,不要抢跑

fork 开启时,提示词会加入纪律:

Don't peek. The tool result includes an output_file path - do not Read or tail it unless the user explicitly asks for a progress check.
Don't race. After launching, you know nothing about what the fork found. Never fabricate or predict fork results in any format.
Writing a fork prompt. Since the fork inherits your context, the prompt is a directive - what to do, not what the situation is.

这段证明 fork 的难点不是“怎么启动子 agent”,而是防止父 agent 污染并行过程:

  • 不偷看中间文件,避免把 fork 的噪音拉回父上下文。
  • 不抢跑预测结果,避免在子 agent 完成前编造结论。
  • fork 继承上下文,所以 prompt 应该是任务指令,不是重复背景。

“不要委派理解”

AgentTool 提示词里最关键的一句:

Never delegate understanding.

它要求父 agent 不能写“based on your findings, fix the bug”这种模糊任务,把综合判断甩给子 agent。父 agent 要先完成理解和分解,再把明确任务交出去。

核心直觉

子 agent 可以执行研究或实现片段,但父 agent 不能把“理解任务本身”的责任外包掉。

技能工具:工具提示词也要管理自己的体积

SkillTool 的特别之处是:它不只控制行为,还控制技能列表本身的 token 成本。

1% 上下文预算

export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01;
export const CHARS_PER_TOKEN = 4;
export const DEFAULT_CHAR_BUDGET = 8_000;

这段代码证明技能列表最多占上下文窗口约 1%。对于 200K token,约等于 8000 字符。

为什么?因为技能列表只是目录,不是技能正文。模型只需要判断是否调用技能,真正内容在调用后加载。

三级截断

formatCommandsWithinBudget() 有三级策略:

级别条件行为
完整保留总描述未超预算全部技能完整展示
描述裁剪超预算但平均描述长度足够非内置技能描述裁剪,内置技能保留完整
仅保留名称平均描述低于 MIN_DESC_LENGTH非内置技能只显示名称

每个条目还有硬上限:

export const MAX_LISTING_DESC_CHARS = 250;

getCommandDescription() 先把单条描述截到 250 字符,再参与总预算。

这说明动态工具/技能目录必须预算感知。否则插件生态越大,第一轮上下文越被目录挤爆。

技能匹配是阻塞要求

SkillTool 的调用协议里有强约束:

When a skill matches the user's request, this is a BLOCKING REQUIREMENT:
invoke the relevant Skill tool BEFORE generating any other response about the task

还有重复加载防护:

If you see a <command-name> tag in the current conversation turn,
the skill has ALREADY been loaded - follow the instructions directly
instead of calling this tool again

这证明 SkillTool 要解决两个常见问题:

  • 匹配技能时,模型不能先自由发挥再加载技能。
  • 技能已加载时,不能重复调用造成循环和上下文浪费。

状态和数据结构

工具提示词核心结构影响
BashTool工具偏好矩阵、Git 协议、沙箱 JSON、sleep 反模式把万能入口限制为受控兜底
FileEditToolgetPreReadInstruction()old_string 唯一性、行号前缀说明确保编辑基于真实文件内容
FileReadToolMAX_LINES_TO_READ、offset/limit、PDF/page 条件能力控制读取体积和能力承诺
GrepToolALWAYS/NEVER、ripgrep 语法、head_limit让搜索走受控工具,避免上下文爆炸
AgentToolagent 列表注入、fork 纪律、委派 prompt 规范控制多 agent 协作质量和缓存稳定
SkillTool1% budget、三级截断、BLOCKING REQUIREMENT控制技能目录体积和调用顺序

设计取舍

取舍点源码体现工程判断
万能工具降级Bash 偏好矩阵Bash 是兜底,不是默认入口
双向约束Bash 说别 grep,Grep 说搜索用我单向提示容易漏,双向闭环更稳
前置条件双层防御Edit prompt + runtime errorprompt 避免浪费,代码兜底
运行能力动态声明isPDFSupported()不向模型承诺运行时做不到的能力
安全默认 + 逃生口head_limit=2500 unlimited默认省上下文,必要时显式放宽
动态内容外移tengu_agent_list_attach降低工具 schema 变化对 prompt cache 的影响
目录预算Skill 1% 上下文预算动态生态不能吞掉工作上下文
阻塞加载Skill BLOCKING REQUIREMENT匹配技能时先加载指令,再开始任务

读源码抓手

  1. 先看 BashTool/prompt.ts 的工具偏好矩阵,确认它如何把模型导向专用工具。
  2. 再看 Git Safety Protocol,注意 NEVERunless explicitly requestsCRITICAL 的组合。
  3. 读沙箱 section,关注 JSON 策略如何内联、dedup()$TMPDIR 归一化如何省 token。
  4. FileEditTool/prompt.ts,把 Read 输出格式和 Editold_string 匹配连起来。
  5. FileReadTool/prompt.ts,确认默认 2000 行、targeted offset、PDF 条件能力。
  6. GrepTool/prompt.ts 和 schema,理解 head_limit 的默认值与逃生口。
  7. AgentTool/prompt.ts,重点读 agent 列表注入方式和 fork 纪律。
  8. 最后读 SkillTool/prompt.ts 的预算和三级截断,学习动态目录如何不挤爆上下文。

小结

- 工具提示词是每个工具自己的局部行为协议。 - BashTool 的核心是把万能入口导向专用工具,并写清 Git、安全、沙箱和等待规则。 - FileEditTool 把“先读后改”做成 prompt 和 runtime 双层约束。 - FileReadTool 和 GrepTool 体现资源预算:默认限制,必要时显式放宽。 - AgentTool 和 SkillTool 说明工具提示词还要处理动态列表、缓存稳定、委派质量和目录预算。