Skip to content

Latest commit

 

History

History
310 lines (179 loc) · 18.6 KB

File metadata and controls

310 lines (179 loc) · 18.6 KB

第 5 章:工具系统内幕

Claude Code 不是一个模型加一个终端。它是一个模型 + 30 多个工具 + 一套安全拦截系统。你在终端里看到的每一次文件读写、每一次搜索、每一次 bash 命令,都是模型调用了某个工具。

理解工具系统,就理解了 Claude Code 行为的底层逻辑——为什么它有时候"不听话",为什么某些命令总被拒,为什么搜索有时快有时慢。


5.1 30+ 工具的三层加载

工具分三层加载,不是所有工具都会出现在每次对话里。哪些工具被加载,直接决定了 Claude 在这次对话里能做什么。

始终加载(约 16 个):

这是核心工具集,每次对话都在。包括:

  • Bash(执行 shell 命令)、FileRead(读文件,图片 PDF notebook 都行)、FileEdit(精确字符串替换)、FileWrite(整文件写入)
  • Glob(按文件名模式搜索)、Grep(按内容搜索)、WebFetch(抓网页)、WebSearch(网络搜索)
  • Agent(创建子 Agent)、TodoWrite(待办任务)、NotebookEdit(编辑 Jupyter notebook)、Skill(调用 Skill)
  • AskUserQuestion(反问你问题)、EnterPlanMode / ExitPlanMode(进出规划模式)、Brief(简洁回复模式)
  • SendMessage(Agent 间通信——发消息给其他 Agent、队友、或通过 bridge/uds 发送)

条件加载(约 15 个):

只在特定条件下出现:

  • ToolSearch 在 MCP 工具多时自动加载
  • EnterWorktree / ExitWorktree 在 git worktree 功能启用时加载
  • Task 系列在 TodoV2 功能启用时加载
  • CronCreate / CronDelete / CronList 在定时任务功能启用时加载
  • PowerShell 只在 Windows 环境出现,Sleep 在需要等待时加载

MCP 工具——动态合并:

你通过 MCP 接入的外部工具,运行时动态合并进来。关键规则:内置工具优先级永远高于 MCP 工具。如果你的 MCP 工具跟内置工具同名,内置工具会覆盖你的。所以给 MCP 工具起名时要避开内置工具名。

SIMPLE 模式:

设环境变量 CLAUDE_CODE_SIMPLE=true,只加载 3 个工具——Bash、FileRead、FileEdit。所有其他工具全部禁用。

适合嵌入到你自己的脚本流水线里,减少干扰,也省 system prompt 空间。一个完整的工具集定义可能占 5000-8000 token,SIMPLE 模式砍到不到 1000。


5.2 工具"说明书"机制

每个工具都有一份完整的 prompt——不是官方文档里的两句话简介,而是一份几百到上千字的详细说明书。这份说明书注入到 system prompt 里,告诉模型什么时候该用、怎么用、哪些绝对不能做。

这个机制的意义在于:Claude 的行为不是模型"自己决定"的,而是说明书"指导"的。理解说明书,就理解了 Claude 为什么总是做某些你觉得多余的事情。

BashTool 的说明书——光要点就有这些:

优先用专用工具。说明书里明确写了:优先用 Read 而不是 cat,用 Grep 而不是 grep,用 Edit 而不是 sed,用 Write 而不是 echo,用 Glob 而不是 find。这不是建议,是硬性指令。

工作目录跨命令持久化。你在第一个 Bash 调用里 cd 到了 /tmp,第二个 Bash 调用还在 /tmp。但 shell 环境变量、alias 不持久——每次调用都是新 shell 进程。

超时默认 120 秒,最大可设 600 秒(10 分钟)。超过 600 秒的命令没法直接跑,需要用 run_in_background 参数后台执行。

多命令策略:独立命令开多个 tool call 并行跑,有依赖关系的用 && 串起来。说明书里甚至举了例子——"如果需要运行 git status 和 git diff,发两个独立的 Bash 调用并行跑"。

git 安全协议 8 条规则(5.7 节详细讲)。

commit 消息模板和 PR 创建模板——这就是为什么 Claude 创建的 commit 格式总是很固定,末尾总带 Co-Authored-By 行。

FileReadTool 的说明书要点:

默认读 2000 行。大文件需要用 offset + limit 参数分段读。支持读图片(模型直接看到图片内容)、PDF(超过 10 页必须指定页码范围,每次最多 20 页)、Jupyter notebook(返回所有 cell 和输出)。

一个重要的优化机制:如果文件内容跟上次读的完全一样,返回 FILE_UNCHANGED_STUB 而不是完整内容。这个设计能省大量 token——你反复读同一个文件时,第二次开始几乎不消耗 token。

FileEditTool 的核心限制:

精确字符串匹配替换,不是正则。old_string 在文件中必须唯一——不唯一就报错,你要提供更多上下文让它唯一。

编辑前必须先 Read 过该文件——没读过就编辑会直接被拒。必须保留原文件的精确缩进,tab 和空格不能混。

这些限制听起来烦,但其实是保护你。想象一下如果 Edit 支持正则替换,一个写错的正则可能改掉文件里所有匹配的地方,包括你不想改的。精确匹配 + 唯一性校验,保证每次只改你想改的那一处。

GrepTool 的实际能力:

底层是 ripgrep,支持完整正则语法。三种输出模式:files_with_matches(默认,只返回文件路径)、content(返回匹配行及上下文)、count(返回每个文件的匹配计数)。

默认最多返回 250 条结果(head_limit=250),传 0 可以不限制但会消耗大量 token。支持 multiline: true 做跨行匹配。


5.3 为什么 Claude 总用 Read 不用 cat

你可能注意到了:让 Claude Code 读文件,它几乎永远用 Read 工具,不用 cat。就算你在 CLAUDE.md 里写"用 cat 读文件"也没用。

原因很简单——BashTool 的说明书里有一段硬性指令:

IMPORTANT: Avoid using this tool to run cat, head, tail, sed, awk, or echo commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user.

这段指令在 system prompt 里。你的 CLAUDE.md 在 user message 里。这个位置差异决定了优先级:system prompt 的指令优先级高于 user message。

所以如果你在 CLAUDE.md 里写了跟工具说明书冲突的指令,Claude 听说明书的,不听你的。这不是 bug,是 prompt 优先级的设计。system prompt 是"宪法",user message 是"建议"。

可执行操作: 不要在 CLAUDE.md 里写跟工具说明书冲突的指令。你改不了模型用什么工具读文件,但你可以指定"读文件时只读 src/ 目录"或者"读文件时用 offset 参数跳过前 100 行"。这种约束不跟说明书冲突,模型会执行。

踩坑经历:有一次我在 CLAUDE.md 里写了"所有文件操作都用 Bash 完成",想让它用 cat、sed 这些我更熟悉的命令。结果 Claude 完全无视了这条指令,继续用 Read 和 Edit。

我花了 20 分钟以为是 CLAUDE.md 没生效,查了半天发现是说明书优先级更高。白折腾。


5.4 BashTool 四层安全拦截

你的命令被拒了?不是 Claude 在耍脾气,是四层安全检查中的某一层拦截了你。理解每一层拦什么,就知道怎么写不会被拒的命令。

第 1 层:命令注入检测——20+ 个独立检测器

这一层防的是恶意注入,但有时候也会误伤正常命令。检测器覆盖以下模式:

命令替换:$()${}、反引号、进程替换 <() >()。这些语法能在命令中嵌入另一个命令,是注入攻击的常用手段。

Unicode 混淆:用长得像的 Unicode 字符伪装正常命令。比如用 Cyrillic 字母 "с" 替换拉丁字母 "c",肉眼看不出来,但执行的是完全不同的命令。

IFS 注入:修改 IFS(Internal Field Separator)变量改变 shell 的分词规则,从而改变命令的解析方式。

proc 访问:读取 /proc 目录的操作会被拦截,因为 /proc 里可能有环境变量和密钥信息。

花括号展开:{a,b,c} 展开攻击。正常的花括号展开是 shell 特性,但可以被滥用来生成恶意命令。

控制字符:隐藏的不可见控制字符,可以改变命令的显示和执行不一致。

行内注释:用 # 隐藏恶意命令的后半部分,让审查者看到的和实际执行的不同。

每个检测器的 ID 用数字编码,不用字符串。这个设计是刻意的——如果用字符串(比如 "unicode_obfuscation"),错误日志里会暴露系统检查了哪些维度,攻击者就知道该绕哪个。

用数字 ID(比如 "detector_7")就无法推断检测内容。

第 2 层:sed 白名单——只允许两种写法

sed 是一个功能极其强大的工具,强大到可以写文件、执行命令。所以 Claude Code 对 sed 做了严格白名单,只允许两种用法:

sed -n 'Np'              # 行打印:必须有 -n 标志,只允许 p 命令
sed 's/pattern/replacement/flags'  # 简单替换:分隔符只允许 /,标志只允许 g/p/i/I/m/M/1-9

以下全部禁止:w/W(写文件)、e/E(执行命令)、花括号代码块(组合多个操作)、多行 sed 脚本(复杂度太高难以安全校验)。

跟 Claude 争 sed 权限是浪费时间。你写了一个稍微复杂的 sed 命令,大概率被拒。直接用 FileEdit 工具——它能做 sed 能做的所有文本替换,而且更安全更精确。

踩坑经历:有一次我想用 sed -i 's/old/new/g' file.txt 批量替换,被拒了。换成 FileEdit 的 replace_all 参数,一行搞定,还更直观。从那以后我再也没试过用 sed。

第 3 层:路径越界检测——30+ 命令的路径参数验证

30 多个常用命令(cd、ls、find、rm、mv、cp、cat、grep、sed、git、jq 等)的路径参数会被自动提取和校验,确保在允许的工作目录范围内。你不能通过 cd ../../.. 跳出项目目录去操作系统文件。

rm 和 rmdir 有额外的危险路径检测。你 rm -rf / 或者 rm -rf ~ 肯定会被拦。就算你用相对路径试图绕过,路径解析后还是会被检测到。

第 4 层:只读命令安全标志白名单

定义了大量命令的安全子命令和标志白名单:

git 只读子命令:status、diff、log、show、branch(只看不改的操作)。 gh 只读子命令:pr list、issue list、pr view 等(只查询不操作)。 docker 只读子命令:ps、images、logs(只查看不执行容器操作)。 ripgrep 安全标志白名单:搜索类标志全部放行。

写操作命令需要用户授权才能执行,只读命令自动通过。这就是为什么 git status 不需要你确认,但 git push 会弹确认框让你手动批准。

实际操作: 遇到命令被拒,先想想是哪一层拦的。命令里有 $() 或反引号?第 1 层。用了复杂 sed?第 2 层。路径超出项目目录?第 3 层。执行了写操作?第 4 层。

定位到哪一层,就知道怎么改写命令。大多数情况下,换用专用工具(FileEdit 替代 sed、FileRead 替代 cat)能绕过 90% 的拒绝。


5.5 ToolSearch 和延迟加载

你装了 10 个 MCP 工具,每个工具的参数定义(JSON Schema)加起来好几千 token。全部塞进 system prompt?每次对话都多花几千 token,太贵了。

Claude Code 的做法是延迟加载:MCP 工具默认只发名字给模型,不发完整的参数 schema。模型知道有这个工具,但不知道它接受什么参数、怎么调用。

当模型想用某个工具时,先调 ToolSearch 拿到完整定义,拿到定义后才能真正调用。

这导致了一个常见问题: Claude 有时候"忘记"某个 MCP 工具。它看到名字了,但因为不知道怎么调用,就选择用别的方式完成任务,或者干脆说"我做不到"。

特别是工具名比较抽象的时候——比如你的 MCP 工具叫 execute_pipeline,模型可能不确定它是干什么的,就不会主动去 ToolSearch。

解决办法有两个:

第一,在 MCP 工具的 metadata 里设置 _meta['anthropic/alwaysLoad'] = true,强制始终加载完整定义。代价是多占一点 system prompt 空间(每个工具几百 token),但保证模型不会"忽略"它。

第二,在 CLAUDE.md 里明确写"当需要做 X 时,使用 Y 工具"。比如"当需要查询数据库时,使用 db_query 工具"。这能提醒模型去 ToolSearch 搜索这个工具的定义。

如果你的 MCP 工具经常"消失"或者 Claude 不主动使用,大概率就是延迟加载的问题。加上 alwaysLoad 配置基本能解决。


5.6 工具并发规则

Claude Code 不是所有工具调用都串行的。它有一套并发控制,最大并行工具调用数是 10 个(内部叫 MAX_TOOL_USE_CONCURRENCY)。

工具类型 并发安全 最大并行数
只读工具(Read、Glob、Grep、WebFetch 等) 10
写操作工具(Edit、Write、Bash 写命令) 1(串行)

这就是你观察到的行为:Claude 经常一口气开 5 个 Grep 搜索并行跑,搜索结果几乎同时返回。但 Edit 文件永远一个接一个来,改完一个再改下一个。

这个设计的原因很直接:两个 Grep 同时搜不同文件,互不干扰。两个 Edit 同时改同一个文件,后一个可能覆盖前一个的修改。为了数据一致性,写操作必须串行。

可执行操作: 如果你想让 Claude 快速完成一批搜索任务,把它们拆成独立的搜索指令——"搜索 src/ 里所有 TODO 注释""搜索 tests/ 里所有失败的测试""搜索 docs/ 里过时的 API 文档"。模型会自动并行执行。

如果你给它一个"搜索 A 的结果去搜 B"的链式任务,它只能串行,因为 B 依赖 A 的结果。


5.7 Git 安全协议

BashTool 说明书里写死了一套 git 安全协议,8 条硬性规则,模型不会违反。

1. 永远不更新 git config。 模型不会帮你设 user.name、user.email 或任何 git 配置。这防止了通过 AI 篡改 git 身份的风险。

2. 永远不 force push。 除非你明确说"force push"。而且如果目标是 main 或 master 分支,会额外弹警告。因为 force push 到主分支会覆盖别人的提交,在团队协作中是灾难性操作。

3. 永远不跳过 hooks。 不会自动加 --no-verify--no-gpg-sign。如果 pre-commit hook 失败了,Claude 会去调查失败原因并修复,而不是跳过 hook。

4. 永远不 amend。 除非你明确说"amend 上一个 commit"。默认总是创建新 commit。这条规则有一个重要的推论——

5. pre-commit hook 失败后创建新 commit,不 amend。 因为 hook 失败意味着 commit 没有发生。如果这时候用 --amend,改的是上一个已经存在的 commit,不是你想要的那个失败的 commit。这个坑很多人踩过。

6. staging 时指定文件名。 不用 git add -Agit add .,而是 git add specific-file.ts。避免意外把 .env、node_modules、大文件提交进去。

7. 不提交可能含密钥的文件。 看到 .env、credentials.json、*.pem 等文件名,Claude 会警告你而不是直接 commit。

8. commit 消息格式通过 HEREDOC 传入。 末尾自动加 Co-Authored-By: Claude 归属行,标记这个 commit 有 AI 参与。

可执行操作: 在 CLAUDE.md 里你可以覆盖 commit 消息的格式,比如"用 conventional commits 格式"或"commit 消息用中文",模型会把你的格式要求叠加到默认模板上。

但你没法让它跳过安全规则——不管你在 CLAUDE.md 里写什么,force push 保护、hook 保护、staging 保护都不会被关闭。


5.8 五个 CLAUDE.md 技巧

知道了工具系统怎么运作,你可以在 CLAUDE.md 里精准调控模型行为。以下 5 个技巧都是顺着工具系统的规则写的,不跟说明书冲突,所以遵守率很高。

1. 指定搜索范围

搜索代码时优先在 src/ 目录下搜索,测试文件在 tests/ 目录。
不要搜索 node_modules/、dist/、.git/ 目录。

不指定的话,Claude 可能 Glob **/* 扫一遍整个项目再开始搜索。一个有 node_modules 的前端项目,不指定范围可能搜出几千个结果,浪费几千 token。指定了范围,搜索结果精准,每次省几百到几千 token。

2. 指定测试命令

运行测试:npm test
运行单个测试:npm test -- --testPathPattern=<文件名>
不要用其他方式运行测试。

Claude 不知道你的测试命令时会猜。猜错了(比如用 jest 而你的项目用 vitest)就浪费一轮对话 + 一次 Bash 调用。写明测试命令,一步到位。

3. 指定 git 工作流

commit 消息格式:conventional commits,中文描述
push 前先 pull --rebase
不要用 squash merge

这些指令叠加在 git 安全协议之上,不冲突。Claude 会同时遵守安全规则和你的格式偏好。

4. 限制文件操作范围

不要修改以下目录的文件:config/、.github/、docker/

.claude/settings.json 的 deny 规则更靠谱(100% 遵守率),但 CLAUDE.md 里写也有效果(实测约 85% 遵守率)。如果是关键目录,两个地方都写——settings.json 做硬限制,CLAUDE.md 做软提醒。

5. 告诉模型你的项目结构

项目结构:
- src/api/ — 后端 API 路由
- src/lib/ — 共享工具函数
- src/components/ — React 组件
- prisma/ — 数据库 schema

这条信息能让模型在第一次搜索时就找对目录,而不是先 Glob **/* 扫一遍整个项目。一个中等项目,这能省 2000-5000 token。

更重要的是省时间——少一轮"先探索项目结构"的搜索循环,直接进入正题。


本章速查

你遇到的情况 原因 怎么办
Claude 不用 cat 读文件 说明书指令优先级高于 CLAUDE.md 别挣扎,Read 工具更好用
sed 命令被拒 只允许行打印和简单替换两种写法 用 FileEdit 工具的 replace_all
bash 命令被拒 四层安全检查之一拦截 定位是哪层,换写法或换工具
MCP 工具偶尔"消失" 延迟加载没触发 ToolSearch 设 alwaysLoad 或 CLAUDE.md 提示
搜索很慢 全项目扫描 CLAUDE.md 里指定搜索目录
Edit 总是串行 写操作最大并行数为 1 设计如此,保证数据一致性
git push 要确认 写操作需要用户授权 安全设计,确认就行
commit 格式不对 默认模板跟你要的不一样 CLAUDE.md 里覆盖格式
commit 末尾有 Co-Authored-By 说明书里的 commit 模板 这是归属标记,保留即可
编辑报错"not unique" old_string 在文件中出现多次 提供更多上下文让匹配唯一