Repository navigation
.claude/skills/** 的 markdown 不被任何门禁扫描 —— check:nul-bytes 只看 JS/TS,check:doc-authoring 的 ROOTS 不含 .claude/ #4890
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 3, 2026 xuyushun441-sys commented
on Aug 3, 2026 CollaboratorMore actions认领(PM 分派)
- session:
session_018iARDqtrhQgz6fVHDeDkbQ - 分支:
claude/issue-4890-nul-gate-covers-all-text - 车道:
domain:devx(落点scripts/check-nul-bytes.mjs)
取方向 1(扫描面从「JS/TS 源文件」改成「所有被 git 跟踪的文本文件」),理由就是本单正文自己给出的那条:该检查拦的是 grep 可见性,那是 grep 的行为、与文件是什么语言无关 —— 按载体划范围才与它自己声明的理由一致。方向 2(显式加
.claude/**/*.md)成本最低但把「为什么是这些文件」原地留着,下一个新目录照漏。方向 3(
check:doc-authoring的ROOTS加.claude)不在本单范围内,按正文的要求单独判:那条检查的规则是面向已发布文档的写作规范,是否适用于内部 agent 指令文件需要先想清楚,为了覆盖率好看硬加会制造一批需要豁免的噪音。dev 若有判断请写进报告,由我另立单,不要捆进本 PR。这一单的底色值得记一句:一条关于裸 NUL 的规则,在写的过程中产生了一个裸 NUL,而负责拦裸 NUL 的门禁看不见它。 与本会话这两天连关的几个是同一族 —— 门禁在跑、且是绿的,但结构上接触不到它的检查对象(#4690 没有 MANIFEST 也 exit 0;#4804 判据写在 stderr 而 gate 从不读;#4835 词表少一个入口,#4772 就从那里溜过去)。所以派发文里把双向证明设成硬约束:必须证明改前对一个含裸 NUL 的
.claude/skills/*.md判绿、改后判红。
Generated by Claude Code
- session:
xuyushun441-sys commented
on Aug 3, 2026 CollaboratorMore actions审核通过 — PR #4907 已取消草稿并挂上 auto-merge(SQUASH)
判据里那句话是这单的核心,已经写进代码注释:
the byte under investigation must never be the reason we decline to investigate
二进制判定先剔除全部 NUL 再解码,因为「有 NUL → 判为二进制 → 不查 NUL」是一个会自我豁免的循环 —— git 自己就掉在里面。多字节 UTF-8 序列不含
0x00,所以剔除不会破坏合法序列。这一步是整个方案成不成立的地方。解码读整文件而不是前缀窗口,同样是有来历的:本脚本自己的文案里就记着 git 只嗅前 8000 字节这个盲区(
protocol.ts的那个 NUL 在第 147230 字节)。复用同一个窗口等于把盲区复制一份。而且 dev 的第一版正是 64 KiB 前缀窗口,当场把两个文件误判成二进制 —— 窗口边界把一个多字节汉字切成了两半;self-test 里留了一个 4000 次重复中文的 fixture 常驻盯这条。踩过再改,比一开始就写对更值得记录。没有扩展名白名单,只有三条按序命中的跳过(非常规文件 / UTF-16-32 BOM / 整文件解不通 UTF-8),所以一个扩展名从没见过的新文本文件默认被扫到 ——
config/weird.frobnicate是这条的常驻证据。这正是不取方向 2 的理由:范围按载体划,新目录不会再漏。双向证明扎实:同一个含裸 NUL 的
.claude/skills/**/SKILL.md,main 上的脚本报OK (2953 tracked source file(s))exit 0,本分支报红并点名文件:3:52 -- first at byte offset 69。测试用的 NUL 全部由Buffer.from([0x00])运行时生成,编辑器里没写出任何裸字节 —— 这单在防的事,实施过程本身没再犯一次。耗时 148ms → 505ms(扫描面 2953 → 4974),半秒级,可接受。全仓 4974 个文本文件零裸 NUL,main 上无既存文件被判红。
共享文件:根
package.json一行,把--self-test &&串进check:nul-bytes,与仓内 9 条既有 script 同形。这一步不能省 —— 不接进去的话新写的自检没人跑,那正是 #4868 那一族(自检与真实调用路径不是同一条)。
⚠️ 更正我在认领评论里的裁定:方向 3 该做,而且我给的排除理由是错的我当时写「
check:doc-authoring的规则是面向已发布文档的写作规范,是否适用于内部 agent 指令文件需要单独判断,硬加会制造一批需要豁免的噪音」。dev 读了scripts/check-doc-authoring.mjs之后指出:它根本不是通用的散文写作规范,而是一条很窄的规则 —— 只在ts /tsx 围栏里匹配绕开defineX()工厂的裸 metadata 字面量(#2035 / ADR-0059)。它管的是代码样例的正确性。所以我那个疑虑的前提不成立。反过来,该脚本自己的文件头写着:Skills are the corpus AI authors from, so a bad sample there is worse than one in app code ——
.claude/skills/**同样是 agent 照抄的语料。实测噪音:
.claude下被跟踪的 markdown 共 4 个,含 ts 围栏代码块的 0 个,would-be violations 0 个。加进ROOTS需要豁免的文件数是零。我是从议题措辞("已发布文档的写作规范")推的,没读脚本 —— 与我这两天在 #4804 上犯过的是同一个错(从议题词汇猜落点/性质,而不是读代码定)。已按 dev 的建议另立单跟踪,范围写作
.claude而非只.claude/skills,并会带上他发现的一个实现细节:该脚本用readdirSync递归而非git ls-files,.claude/worktrees/虽在.gitignore里仍会被 walk 到,需要进SKIP_DIRS,否则会扫到别的 agent 的 worktree。
Generated by Claude Code
- added a commit that references this issue
on Aug 4, 2026 - added 6 commits that reference this issue
on Aug 4, 2026 - added a commit that references this issue
on Aug 12, 2026
发现于 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 系列 —— 同属「门禁覆盖面与它自己声明的理由不一致」