发现于 PR #4885(#4882,给 pm-dispatch skill 增补运行经验)的实施过程。未认领,本会话只记录不派发(属 devx 车道)。
事情是这样发生的
那个 PR 要写的其中一条经验,正是「不要写裸 NUL 字节」。dev 在写这条规则的过程中,Edit 工具把转义写成了一个真的裸 NUL 字节,落在 .claude/skills/pm-dispatch/SKILL.md 第 626 行。
它没有被 check:nul-bytes 抓到 —— 是 dev 自己额外跑的控制字符扫描(grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]')抓到的。
一条关于裸 NUL 的规则,在写的过程中产生了一个裸 NUL,而负责拦裸 NUL 的门禁看不见它。
两个覆盖缺口(均已核实脚本源码)
| 门禁 |
扫描范围 |
.claude/skills/*.md |
scripts/check-nul-bytes.mjs |
只扫 JS/TS 源文件(本次实跑报告 2986 tracked source file(s)) |
❌ 不覆盖 |
check:doc-authoring |
ROOTS = ['skills', 'content'] |
❌ 不覆盖(是顶层 skills/,不是 .claude/skills/) |
| eslint |
files glob 只有 **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} |
❌ 不覆盖 md |
也就是说 .claude/ 下的所有 markdown 处在全部门禁之外。
为什么这不是小事
check-nul-bytes.mjs 自己的报错文案把理由写得很清楚:
A raw NUL makes grep/ripgrep treat the entire file as binary and silently return ZERO matches, so the file drops out of code search and out of every grep-based lint.
这个后果与文件是什么语言无关 —— 它是 grep 的行为,不是 JS 的行为。而 .claude/skills/** 恰恰是每个 agent 会话都会加载、并且经常被 grep 检索的内容:一个混进 SKILL.md 的裸 NUL 会让整份操作手册对 grep -r 隐形,而 agent 拿不到它本该遵守的规则,且没有任何信号。
这与本仓正在系统性关掉的那一类是同一个形状:声明了一条规则(不许裸 NUL),但强制它的机制覆盖不到规则自己所在的地方。
建议方向(择一,倾向 1)
- 把
check-nul-bytes.mjs 的扫描范围从「JS/TS 源文件」改成「所有被 git 跟踪的文本文件」(用 git ls-files + 二进制探测排除真正的二进制资产)。最贴合该检查自己声明的理由 —— 它拦的是 grep 可见性,不是语法。代价是扫描面变大,需要确认耗时可接受。
- 保持范围不变,但把
.claude/**/*.md 显式加进去。成本最低,但把「为什么是这些文件」这个问题继续留在原地,下一个新目录还会漏。
- 顺带考虑
check:doc-authoring 的 ROOTS 要不要加 .claude。⚠️ 这条需要先判断:该检查的规则(面向已发布文档的写作规范)是否适用于内部 agent 指令文件 —— 如果不适用,不要为了「覆盖率好看」硬加,那会制造一批需要豁免的噪音。这一条不要和方向 1/2 捆绑决定。
一条独立的观察
本次是人(agent)在写规则时违反了规则,靠一次额外的、非常规的自检才发现。这说明:凡是「工具生成的内容里可能混入不可见字节」的场景,检查范围应当按载体(所有文本文件)而不是按用途(源代码)来划。
关联
发现于 PR #4885(#4882,给
pm-dispatchskill 增补运行经验)的实施过程。未认领,本会话只记录不派发(属 devx 车道)。事情是这样发生的
那个 PR 要写的其中一条经验,正是「不要写裸 NUL 字节」。dev 在写这条规则的过程中,Edit 工具把转义写成了一个真的裸 NUL 字节,落在
.claude/skills/pm-dispatch/SKILL.md第 626 行。它没有被
check:nul-bytes抓到 —— 是 dev 自己额外跑的控制字符扫描(grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]')抓到的。一条关于裸 NUL 的规则,在写的过程中产生了一个裸 NUL,而负责拦裸 NUL 的门禁看不见它。
两个覆盖缺口(均已核实脚本源码)
.claude/skills/*.mdscripts/check-nul-bytes.mjs2986 tracked source file(s))check:doc-authoringROOTS = ['skills', 'content']skills/,不是.claude/skills/)filesglob 只有**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}也就是说
.claude/下的所有 markdown 处在全部门禁之外。为什么这不是小事
check-nul-bytes.mjs自己的报错文案把理由写得很清楚:这个后果与文件是什么语言无关 —— 它是 grep 的行为,不是 JS 的行为。而
.claude/skills/**恰恰是每个 agent 会话都会加载、并且经常被 grep 检索的内容:一个混进 SKILL.md 的裸 NUL 会让整份操作手册对grep -r隐形,而 agent 拿不到它本该遵守的规则,且没有任何信号。这与本仓正在系统性关掉的那一类是同一个形状:声明了一条规则(不许裸 NUL),但强制它的机制覆盖不到规则自己所在的地方。
建议方向(择一,倾向 1)
check-nul-bytes.mjs的扫描范围从「JS/TS 源文件」改成「所有被 git 跟踪的文本文件」(用git ls-files+ 二进制探测排除真正的二进制资产)。最贴合该检查自己声明的理由 —— 它拦的是 grep 可见性,不是语法。代价是扫描面变大,需要确认耗时可接受。.claude/**/*.md显式加进去。成本最低,但把「为什么是这些文件」这个问题继续留在原地,下一个新目录还会漏。check:doc-authoring的ROOTS要不要加.claude。一条独立的观察
本次是人(agent)在写规则时违反了规则,靠一次额外的、非常规的自检才发现。这说明:凡是「工具生成的内容里可能混入不可见字节」的场景,检查范围应当按载体(所有文本文件)而不是按用途(源代码)来划。
关联
scripts/check-nul-bytes.mjs的既有文案与packages/rest/src/rest-server.ts:1065的转义约定spec/src/cloud/tenant.test.ts的 #4739 导出面用例贴着 5s 超时 —— 今晚已两次把不相干的 PR 踢出合并队列 #4796 系列 —— 同属「门禁覆盖面与它自己声明的理由不一致」