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 个字符的源码文本。
�、\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 ✓ |
// 错误:工具 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 传输层会吃掉一层。