Skip to content

ci(lint): reject raw NUL bytes in tracked sources#3135

Merged
os-zhuang merged 1 commit into
mainfrom
claude/vibrant-blackburn-348247
Jul 17, 2026
Merged

ci(lint): reject raw NUL bytes in tracked sources#3135
os-zhuang merged 1 commit into
mainfrom
claude/vibrant-blackburn-348247

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

Adds scripts/check-nul-bytes.mjs and wires it into the lint job, so a raw NUL (0x00) in a tracked source file fails CI.

Why

A single literal U+0000 byte makes grep/ripgrep classify the whole file as binary and silently return zero matches. grep -n saveMetaItem packages/metadata-protocol/src/protocol.ts reported nothing despite 16 real hits — a core protocol file invisible to code search and to every grep-based lint, with no error to say so.

Nothing caught this. git decides binary-ness from the first 8000 bytes only, and protocol.ts carried its NUL at offset 147230, so it kept diffing as ordinary text through review. That blind spot is how six separate files accumulated the same defect before #3127 fixed them. This guard is what keeps them from coming back.

What it does

Scans tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.cts/.mts files (git ls-files; generated and vendored output excluded). On a hit it reports file:line:column plus the byte offset — the location grep fundamentally cannot give you — and points the author at the \u0000 escape form and the existing convention at packages/rest/src/rest-server.ts:1065.

Verification

Verified by replaying the real defect, not just a synthetic one. Restoring protocol.ts to its pre-#3127 state reproduces the original exactly and shows why it survived:

grep -n saveMetaItem 0 lines (16 real hits, grep -a)
git diff --numstat ordinary text, 1 insertion / 1 deletion — review sees nothing
this guard red, exit 1protocol.ts:3054:72, byte offset 147230

The reported 3054:72 was confirmed against an independent oracle, and the offending line is exactly the composite-key separator. Also checked: a NEW file with 2 NULs is caught (plural wording correct); a .tsx file is caught; a file using the correct escape form passes; an untracked file is ignored; and a tracked file under an excluded dir stays ignored even when force-added — so the exclusion does real work rather than riding on .gitignore.

Clean tree: green across 2261 files in ~0.2s. ESLint clean.

Notes

  • Extension list is a superset of the six in the original report: .jsx/.cjs are the same class of hand-authored source and match zero files today, so including them closes a future gap at no cost.
  • The guard is in its own scope and passes its own check — every occurrence of the escape sequence in it is an escape, never the byte.
  • Authoring hazard, worth knowing: agent/editor tooling readily materializes a typed \u0000 into a real NUL. It happened twice while writing this PR — a Bash call was rejected for containing control characters because the sequence in my own comments had already become real NULs. Build such strings programmatically (chr(92) + 'u0000') rather than typing them.
  • No changeset: CI tooling only, no user-facing or published-package change (skip-changeset).

🤖 Generated with Claude Code

A single literal U+0000 byte makes grep/ripgrep classify the whole file as
binary and silently return zero matches: `grep -n saveMetaItem
packages/metadata-protocol/src/protocol.ts` reported nothing despite 16 real
hits, hiding a core protocol file from code search and from every grep-based
lint -- with no error to say so.

Nothing caught this. git decides binary-ness from the first 8000 bytes only,
and protocol.ts carried its NUL at offset 147230, so it kept diffing as
ordinary text through review. That blind spot is how six separate files
accumulated the same defect before #3127 fixed them.

Add scripts/check-nul-bytes.mjs, wired into the lint workflow beside the other
guards. It scans tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.cts/.mts sources
(generated and vendored output excluded), reports each offender as
file:line:column plus the byte offset -- the location grep cannot give you --
and points the author at the \u0000 escape form and the existing convention at
packages/rest/src/rest-server.ts:1065.

Verified by replaying the real defect rather than only a synthetic one:
restoring protocol.ts to its pre-#3127 state turns the guard red at 3054:72
(offset 147230, confirmed against an independent oracle) while git still diffs
that file as ordinary text. The fixed tree is green across 2261 files in ~0.2s.
A tracked file under an excluded dir stays ignored even when force-added, so
the exclusion is doing real work rather than riding on .gitignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Jul 17, 2026
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jul 17, 2026 12:27pm

Request Review

@github-actions github-actions Bot added size/m ci/cd dependencies Pull requests that update a dependency file labels Jul 17, 2026
@os-zhuang
os-zhuang merged commit 1376c5f into main Jul 17, 2026
18 of 19 checks passed
@os-zhuang
os-zhuang deleted the claude/vibrant-blackburn-348247 branch July 17, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant