fix(scripts): check:nul-bytes 按载体扫描所有被跟踪的文本文件 (#4890) - #4907
Merged
Conversation
这道门禁自己的报错文案写着它拦的是什么:一个裸 NUL 让 grep/ripgrep 把整个 文件当成二进制、静默返回零匹配。那是 grep 的行为,与文件是什么语言无关 —— 但扫描面此前是一份 JS/TS 扩展名清单,范围(用途)与理由(载体)对不上。 代价是 .claude/ 下的全部 markdown 同时落在三道门禁之外。#4890 就是这么暴露 的:PR #4885 要写的规则正是「不要写裸 NUL」,而写的过程中一个真的裸 NUL 落进 了 .claude/skills/pm-dispatch/SKILL.md,这道门禁报 OK。 改为扫描所有被 git 跟踪的文本文件。二进制判据是内容判断而非扩展名清单: 非常规文件(symlink / gitlink)跳过;UTF-16/32 BOM 开头跳过(那种编码里 NUL 是结构性的);其余先剔除 NUL 字节、再整文件按 UTF-8 严格解码,解不通才算 二进制。先剔除 NUL 是为了打破「有 NUL 所以是二进制所以不查 NUL」这个 git 掉 进去的循环;整文件解码而非只看前缀,是因为 git 只嗅前 8000 字节正是本文案里 记着的盲区。 同时按仓内惯例把 --self-test 接进 check:nul-bytes 脚本,自检在临时 git 仓库 里跑真实的 scan() 路径。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 15:45
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4890
为什么按载体(所有文本文件)而不是按用途(源代码)划范围
这道门禁自己的报错文案已经把理由写清楚了:
这个后果是 grep 的行为,不是 JavaScript 的行为。它落在 markdown、YAML workflow、
.env.example上完全一样,落在一个扩展名还没被发明出来的文件上也一样。但它的扫描面此前是一份 JS/TS 扩展名清单 —— 范围(用途)与它自己声明的理由(载体)对不上。对不上的代价,是
.claude/下的全部 markdown 同时落在三道门禁之外:.claude/skills/*.mdcheck:nul-bytescheck:doc-authoringROOTS = ['skills', 'content'](顶层skills/)filesglob 只有 JS/TS 扩展名而 #4890 正是这么暴露的:PR #4885 要写的规则之一就是「不要写裸 NUL」,写的过程中 Edit 工具把转义写成了一个真的裸 NUL,落在
.claude/skills/pm-dispatch/SKILL.md,而这道门禁报 OK —— 靠一次额外的、非常规的控制字符扫描才发现。一条关于裸 NUL 的规则,在写的过程中产生了一个裸 NUL,而负责拦裸 NUL 的门禁看不见它。 一份带裸 NUL 的 SKILL.md 对grep -r隐形,agent 拿不到它本该遵守的规则,且没有任何信号。方向 2(显式把
.claude/**/*.md加进扩展名清单)成本更低,但把「为什么恰好是这些文件」原地留着 —— 下一个新目录照漏一遍,只是把同一个问题往后挪一格。二进制判据:内容判断,不是扩展名白名单
扩展名白名单就是上面那个缺陷换个地方留着,所以判据全部走内容。一个被跟踪的 blob 默认被扫,只有以下三条按顺序命中才跳过:
lstat而不是stat:本仓根目录的core就是一个 symlink,跟进去在某些检出下直接抛异常。fatal: true),解不通 —— 才算二进制。第 3 条是全部判据,其中两个性质是关键:
0x00,所以剔除 NUL 不会破坏任何合法序列。)protocol.ts的 NUL 在第 147230 字节)。在这里复用同一个窗口等于把盲区原样复制一份。实现过程中这一点被实测打过脸:第一版用 64 KiB 前缀窗口,packages/formula/CHANGELOG.md与plugin-approvals/src/approval-service.ts被误判成二进制 —— 窗口边界把一个多字节汉字切成了两半。自检里留了一个跨窗口的长中文 fixture 盯这条。由此得到的性质,正是扩展名清单不可能有的:一个扩展名从没见过的新文本文件默认被扫到 —— 它能按 UTF-8 解码,所以它是文本。自检里的
config/weird.frobnicate就是这条的常驻证据。今天真正掉出去的只有 4 个 PNG + 1 个 ICO。另外刻意没有加逐文件豁免口子:本仓没有任何被跟踪文件带合法的裸 NUL;真有一天需要,那应当是一次摆到明面上的决定,而不是 skip-list 里悄悄多一行。
双向证明
一个只观察到绿的门禁,和一个什么都匹配不上的门禁,从外部无法区分。所以两个方向都跑了。测试文件用脚本在运行时生成(编辑器里绝不写出真的裸 NUL —— 那正是本单在防的事,而且会污染 diff),验完即删,没有提交进仓库。
同一个被 git 跟踪的
.claude/skills/*.md:改前判绿(exit 0),改后判红并点名文件与line:column:byte offset。自检接进脚本 + 耗时实测
按仓内既有惯例(
check:route-envelope/check:published-files/check:wildcard-fallthrough等 9 条都是这个形状),把--self-test接进了check:nul-bytes:这是本 PR 唯一动到根
package.json的一行(本仓最热的冲突点之一,故显式说明);.github/workflows/没有改动 —— lint.yml 早已调用pnpm check:nul-bytes,自检因此自动进了 CI。自检在临时目录里
git init一个真仓库、写好 fixture、git add之后调用与main()同一个scan()函数(#4868 的教训:自检与真实调用路径必须是同一条),16 条断言覆盖:.claude/markdown、TS 源、未知扩展名、PNG/ICO、UTF-16、悬空 symlink、dist/排除、跨窗口长中文、NUL 的 line:col 定位、classify()三条直判。耗时(同一容器,各跑 3 次取范围):
扫描面 +68%,耗时约 3.3×,绝对值仍在半秒级 —— 对一条 CI 门禁完全可接受,不需要退回方向 2。
pnpm check:nul-bytes(含自检、含 pnpm 启动开销)端到端 1.67s。main 上没有任何既存文件被判红 —— 全仓 4974 个文本文件零裸 NUL,
--list显示掉出扫描面的就是那 4 个 PNG、1 个 ICO 与根目录的coresymlink。明确不在本 PR 范围内
方向 3(
check:doc-authoring的ROOTS加.claude)按分派要求不做,判断写在报告里由 PM 另立单。Generated by Claude Code