Skip to content

6.5 FileRead / FileEdit / NotebookEdit — 先读后写强制

前置阅读6.1 全景 · 6.3 治理流水线


学习目标

完成本节学习后,你应该能够:

  1. 解释 FileEditTool 强制先 FileReadTool 的产品与安全理由。
  2. 描述「盲改拦截」在实现上的几种形态:会话缓存、ETag、版本向量。
  3. 对比 文本 FileEditNotebookEdit 在 Schema 与冲突处理上的差异。
  4. 列举 路径规范化、.. 穿越、符号链接跟随等常见校验点。
  5. 文件三件套接入 PreToolUse / 权限:按扩展名、目录、行数阈值分级。

生活类比:手术核对清单

先读后改手术前核对患者手环:你不能凭记忆动刀。FileEdit 在没有当前文件内容上下文(或等价的版本令牌)时拒绝执行,就像未核对 ID 拒绝麻醉——麻烦一秒,避免灾难级误切。

Notebook 则是「多部位手术」:每个单元格有独立 ID,改错一格不影响整本病历的页码体系(但若结构错乱仍会事故)。


三工具职责表

工具主要职责典型输入典型输出
FileRead读文本/二进制元数据path, offset?content, truncated, lineCount
FileEdit补丁/替换/整段写path, old/new 或结构化 diffapplied, conflicts
NotebookEdit.ipynb 单元cellId, source, cellType?notebookVersion

强制先读:策略表

机制做法优点缺点
会话指纹近期 FileRead 记录 path + hash实现快会话外不共享
显式 readTokenRead 返回 token,Edit 必须带上强一致API 冗长
版本号文件 mtime / content hash可检测并发修改时钟/缓存问题

源码片段:盲改拦截(概念)

typescript
interface SessionReadCache {
  path: string;
  contentHash: string;
  readAt: number;
}

const recentReads = new Map<string, SessionReadCache>();

async function fileReadTool(input: { path: string }): Promise<{ content: string; readToken: string }> {
  const content = await fs.readFile(input.path, "utf8");
  const contentHash = sha256(content);
  const readToken = sign({ path: input.path, contentHash, ts: Date.now() });
  recentReads.set(input.path, { path: input.path, contentHash, readAt: Date.now() });
  return { content, readToken };
}

async function fileEditTool(input: { path: string; readToken: string; patch: string }) {
  const verified = verifyReadToken(input.readToken);
  if (!verified.ok) {
    return { error: "blind_edit_blocked", hint: "请先使用 FileRead 获取 readToken" };
  }
  const snap = recentReads.get(input.path);
  if (!snap || snap.contentHash !== verified.payload.contentHash) {
    return { error: "stale_read", hint: "文件已变化请重新读取" };
  }
  // ... 应用 patch
}

真实产品可能用 更短的 nonce把 read 结果缓存在宿主侧;思想一致:无读凭证则拒绝写


NotebookEdit:结构化编辑

Notebook 是 JSON,编辑应尽量避免「整文件字符串替换」:

typescript
const NotebookCellEdit = z.object({
  path: z.string(),
  cellId: z.string(),
  newSource: z.string(),
  mergeOutputs: z.boolean().optional(),
});

生活类比:改幻灯片某一页的备注,不应重排整份 PPTX 的 XML——应定位到 slideId


Mermaid:先读后写状态机


路径与安全校验清单

检查项说明
path.resolve + 仓库根前缀.. 逃逸
符号链接是否跟随、是否允许跳出根
二进制大文件读时截断、禁止编辑
.env / 密钥文件更高权限或脱敏展示
行尾与编码统一 LF / utf8 减少伪冲突

PreToolUse 钩子示例(伪代码)

typescript
hooks.preToolUse = async ({ tool, input }) => {
  if (tool === "FileEdit") {
    const p = (input as any).path as string;
    if (p.endsWith(".pem") || p.includes(".ssh/")) {
      return { action: "deny", reason: "敏感路径需人工模式" };
    }
  }
  return { action: "allow" };
};

与 Glob / Grep 的协作

步骤工具
发现候选文件Glob
确认内容上下文Grep / FileRead
小范围修改FileEdit
笔记本实验NotebookEdit

冲突与合并(概念表)

场景策略
同时编辑hash 不匹配 → 要求重读
patch 不匹配返回 hunks 失败列表
Notebook cell 删除cellId 不存在 → 明确错误

遥测建议

事件字段
file_readbytes, truncated
file_edithunkCount, applyResult
blind_edit_blockedpath 哈希化

常见反模式

反模式后果
允许无读直接写覆盖他人变更、模型幻觉 patch
整文件覆盖大仓库费用与审查困难
Notebook 当纯文本改JSON 结构损坏

小结

  • FileReadFileEdit 的前置条件;盲改拦截把「幻觉式修改」变成可恢复错误
  • NotebookEdit 应以 cell 身份 为第一公民。
  • 路径与敏感文件策略应落在 validateInput / PreToolUse

自测题

  1. readToken 与 Git 本身提供的版本控制有何互补关系?
  2. 若多进程并发写同一文件,仅靠会话缓存是否足够?
  3. Notebook 输出区(outputs)是否应默认清空以防泄露旧图?

上一节6.4 BashTool · 下一节6.6 搜索工具

本项目仅用于教育学习目的。Claude Code 源码版权归 Anthropic, PBC 所有。