Tools 工具组

FileWriteTool:写入文件

它解决的是“整文件写入”问题

FileWriteTool 适合两种典型场景:

  • 新建一个文件
  • 用一整块新内容覆盖一个现有文件

如果说 FileEditTool 更像外科手术,
FileWriteTool 更像“整块材料重新铺设”。

Claude Code 明确把这两件事拆成了不同工具,而不是统统交给 shell。
这就是它工程设计上的一个细节点。

源码里最关键的输入定义

tools/FileWriteTool/FileWriteTool.ts

const inputSchema = z.strictObject({
  file_path: z.string().describe('The absolute path to the file to write'),
  content: z.string().describe('The content to write to the file'),
})

它的输入非常干净:

  • 写到哪里
  • 写什么内容

这说明 Write 的职责就是完整写入,而不是做复杂的局部替换逻辑。

它的输出比你想的更丰富

outputSchema 里不仅返回最终内容,还返回:

type: z.enum(['create', 'update'])
structuredPatch: z.array(hunkSchema())
originalFile: z.string().nullable()
gitDiff: gitDiffSchema().optional()

也就是说,Write 并不是“静默覆盖然后就没了”。
它依然会保留:

  • 这是创建还是更新
  • 原文件是什么
  • 差异补丁是什么
  • Git diff 视角下变化如何

这很关键,因为它让整文件写入依然处于 Claude Code 的可审计链路里。

一张图看写入链路

加载图表中...

它也会检查文件是否过期

Edit 一样,Write 也不是无脑覆盖。
源码里有一段很重要的校验逻辑:

if (!readTimestamp || readTimestamp.isPartialView) {
  return {
    result: false,
    message: 'File has not been read yet. Read it first before writing to it.',
  }
}

这说明对于已存在文件,Claude Code 很强调:

  • 先读
  • 再写

这能防止基于过期内容覆盖别人刚改过的文件。

它为什么还要接入 diff 和 Git

源码里可以看到:

import { countLinesChanged, getPatchForDisplay } from '../../utils/diff.js'
import { fetchSingleFileGitDiff } from '../../utils/gitDiff.js'

这说明即使是整文件写入,Claude Code 也不把它当作“黑盒操作”,而是会继续:

  • 统计改动行数
  • 生成可展示 patch
  • 关联 Git 视角的变化

这样前端和后续模型都能更清楚地理解这次写入发生了什么。

它和 FileEditTool 的分工

这个区别值得反复强调:

  • Edit:局部修改已有内容
  • Write:创建文件或整文件重写

如果模型只是想改一个函数,Write 往往会显得过重;
但如果是生成新配置文件、新组件骨架、新文档,Write 就非常合适。

一次典型使用路径

加载图表中...

最容易误解它的地方

误解一:Write 只是“更方便的 echo > file”

不是。
它接入了权限、文件时序、diff 和 Git 视图。

误解二:Write 比 Edit 更强,所以更应该优先用

也不对。
Claude Code 更强调职责清晰,而不是“越万能越好”。

误解三:Write 只适合新文件

不完全对。
它也能更新已有文件,但更适合整块重写,而不是局部小修。

小结

FileWriteTool 真正值得学的地方是:

Claude Code 连“整文件写入”都没有降级成简单 shell 重定向,而是仍然纳入了受控、可审计、可 diff 的工具系统。

这也是 Claude Code 整体设计非常“工程化”的地方。