返回博客

Write 工具的 JSON 转义陷阱: 变成真实 NUL 字节写进文件

在把一段老 JS 工具函数迁移到 TypeScript 时,我用 AI 的 Write 工具生成了新文件。代码看起来完全正确,tsc --noEmit 也跑绿了——但之后用 grep 去搜文件里的任何关键字,一行结果都没有。换 rg 一跑,看到了这行:

binary file matches (found '\0' byte around offset 28885)

源码文件变成了二进制。

现象

  • Read 工具显示文件内容一切正常,能看到 .join('�') 的源码字面量。
  • grep -n "join" lib.ts 无输出——文件被 grep 当成二进制跳过。
  • rg -n "join" lib.ts 提示 binary file matches,说明文件里有 NUL 字节(0x00)。
  • tsc --noEmit 和 ESLint 全部通过,零报错。
  • 同一份代码如果写到 .py 文件,Python 解释器直接抛 SyntaxError: source code cannot contain null bytes
  • 运行时行为悄悄漂移:.join('�') 的连接符长度从预期的 6 变成了 1。

根因

Write 工具(以及 Edit 工具的 new_string)的 content 字段是一个 JSON 字符串,按 RFC 8259 §7 规范,JSON 解析器会在把字节交给磁盘之前先解码 \uXXXX

也就是说,如果你在工具调用里写:

].join('�')

JSON 传输层会把 解码成真实的 U+0000 NUL 字符,然后再写盘。磁盘上得到的是一个含有 0x00 字节的文件——而你想要的是含有 这 6 个字符的源码文本。

坑:单反斜杠转义在 Write content 里会被 JSON 层消费掉

\xHH\n(当你想在正则字面量里保留源码 \n)等序列,在 Write/Edit 工具的 JSON 字段里只写一个反斜杠,意味着 JSON 解析器把它解码成对应的字符——NUL、原始字节、换行符——写进文件。

最隐蔽的地方是:Read 工具把 NUL 显示为空格,视觉检查毫无异常;tsc 和 ESLint 也不报错,因为字符串字面量里本来就允许 NUL。唯一的线索是 rg/grep 把文件识别为二进制。

正解

向 Write/Edit 工具传入 content 时,凡是想在文件里保留为字面源码的转义序列,一律把反斜杠加倍:

| 想写进文件的 6 字符序列 | 工具 content 里应写 | 磁盘上得到 | |------------------------|---------------------|-----------| | | \\u0000 | ✓ | |   | \\u00a0 |   ✓ | | \xc2 | \\xc2 | \xc2 ✓ | | \n(正则源码) | \\n | \n ✓ |

正解:Write content 里用双反斜杠保留转义序列
// 错误:工具 content 里这样写
].join('�')   // JSON 解码 → 文件含 NUL 字节
 
// 正确:双反斜杠
].join('\\u0000')  // JSON 解码后 → 文件含字面量 � (6 chars)

同理,NBSP 正则:

// 错误
text.replace(/ /g, ' ')
 
// 正确
text.replace(/\\u00a0/g, ' ')

排查

写完文件后,一条命令即可验证是否含 NUL:

python3 -c "
data = open('lib.ts', 'rb').read()
print('NUL count:', data.count(b'\\x00'))
"

或者强制 rg 文本模式对比:

rg -na "join" lib.ts   # -a 强制文本模式;能匹配但普通 rg 没有输出 → 文件含 NUL

若已写坏,用 Python 二进制模式修复比再用 Edit 工具更稳——Edit 的 old_string 同样面临 JSON 解码,匹配不到坏字节:

path = 'lib.ts'
data = open(path, 'rb').read()
NUL = b'\x00'
data = data.replace(b".join('" + NUL + b"')", b".join('\\\\u0000')")
open(path, 'wb').write(data)

一句话外卖

凡是你想让文件源码里看到反斜杠的地方,传给 Write/Edit 工具时就要写两个反斜杠——JSON 传输层会吃掉一层。