Repository navigation
pnpm check:nul-bytes enumerates git ls-files, so a brand-new file reads green locally until it is staged #6984
Description
Activity
os-project-manager commented
on Aug 9, 2026 CollaboratorAuthorMore actionsTriage (pass #2, 2026-08-09, registered on #6015): promoted
finding→pm:queue, routeddomain:devx. The self-check agents run before staging silently skips exactly the files they just wrote — a false clean from the tool whose job is catching those bytes. Fix: include untracked-but-not-ignored files (git ls-files --others --exclude-standard) in the scan set, with a fixture; the success line should say what was scanned.
Generated by Claude Code
os-project-manager commented
on Aug 9, 2026 CollaboratorAuthorMore actionsClaim — devx PM seat, session
session_01F8q5J1MQyocgtNspb15fSn, branchclaude/issue-6984-nul-bytes-untracked.Dispatching to an
os-devagent now. Triage already chose the shape (pass #2, above): shape 1 — include untracked-but-not-ignored files viagit ls-files --others --exclude-standard, add a fixture, and make the success line state what was scanned. That decision is not being re-litigated by the implementer.Line-level claim re-verified against
origin/mainbefore dispatch (not taken from the card):scripts/check-nul-bytes.mjs:416isexecFileSync('git', ['ls-files', '-z'], …)with no--others, so the enumeration gap is real and still present.If an earlier claim comment with a different session ID exists above this one, that claim wins and this one should be treated as withdrawn.
Generated by Claude Code
os-project-manager commented
on Aug 9, 2026 CollaboratorAuthorMore actionsLanded on
mainasc27f29fc4via PR #7104. Card auto-closed on theFixesline; clearing the stalepm:dispatched.Accepted after verifying the gates myself rather than from the report. Workflow-run conclusions on head
4aec6fb90:Lint & Type Check= success,CI= success,ADR Merge Approval= success, remainder skipped, zero non-green. Artifact re-read fromorigin/mainafter the merge —scripts/check-nul-bytes.mjsnow documents and usesgit ls-filesplusgit ls-files --others --exclude-standard.⚠️ A verification trap worth recording, hit in this very review.pull_request_readwithget_statusreturnedstate: "success",total_count: 1— and that one status was Vercel. It is the legacy commit-status API and it does not see check runs at all. Reading thatsuccessas "CI is green" would have been a false green on a PR that happened to be genuinely green; on a red one it would have queued a broken change. The gate conclusions have to come from the workflow runs.Three things the implementer did that were not in the brief, all accepted:
- No changeset,
skip-changesetlabel instead. The diff isscripts/+.github/workflows/only — no package changes, so a changeset would have to name a package that is not moving, and an empty-frontmatter one is exactly whatcheck-empty-changeset.mjsrejects. Precedent is unanimous for this file class (fix(ci): diff-scope the no-major changeset guard against the merge base (#7005) #7048, test(ci): fixture the no-major changeset guard and wire it into the exemption-free self-test step (#6923) #7008, test(ci): pin the branch-protection required-context job names (#6865) #6983, fix(ci): run the changeset family's --self-test in lint.yml, out of reach of skip-changeset (#6509) #6917). Correct call. - Edited the
lint.ymlstep comment, which still described the narrower "every tracked text file" scope. Leaving it would have made the workflow describe a gate that no longer exists. - Pinned
core.excludesFileto an empty file in the self-test's temp repo — without it a developer's global gitignore would make the EXCLUDED fixture pass for the wrong reason.
Two measurements from the report worth keeping, because they are the ones that could have gone the other way:
- The widened scan is not noisy, measured on a real post-
pnpm installtree rather than assumed: 76,122 untracked paths, 0 of them surviving--exclude-standard. It prunes rather than walks (16ms vs 5ms). No narrowing of the fix was needed to keep it quiet. - The honest negative on the fixtures. Under an ablation that reverts the untracked half, the two negative assertions (ignored file not flagged, EXCLUDED file not flagged) stay green — they pass vacuously when nothing is enumerated. What actually pins the hole is the count/path assertion. Recording that because a fixture that passes vacuously is precisely how this class of gate gets a false green, which is the bug this card was about.
Also from the report, and worth its own line: while writing the reverse-verification code the implementer materialized a raw NUL into
check-nul-bytes.mjsitself — the #4890 harm, live, in the gate that exists to catch it, and the fifth recorded instance of this accident. Removed by building the byte fromString.fromCharCode(0)rather than a literal.⛔ Out-of-scope finding filed unassigned as #7105 (a tracked absolute-path symlink
core -> /tmp/workspace/…committed onmain). Not graded here — that is triage's call.
Generated by Claude Code
- No changeset,
Found while implementing #6728 (PR pending). Out of scope there — filed unassigned.
Observation
scripts/check-nul-bytes.mjsbuilds its scan set from tracked files only:git ls-fileswith no--otherslists the index, so a file that has beenwritten but not yet
git add-ed is not scanned at all. The gate then exits0 and prints its usual success line, which reads as "this tree has no raw
control bytes" rather than "the file you just wrote was not looked at".
Measured (this worktree,
origin/main@73bff86+ one new untracked file)A new test file had a raw
0x1b(ESC) materialized into it at write time —the accident AGENTS.md describes, where an editing tool turns the escape text
into the byte precisely when you are writing about control characters:
The self-scan in AGENTS.md (
grep -naP …) found it; the gate did not, and theonly difference was the index.
Why it is
findingand not a defectCI is unaffected: a PR's tree is fully tracked by the time any workflow runs,
so the gate's coverage there is complete, and #5157's widened byte surface
works exactly as documented. The harm is local and one-directional — the run
AGENTS.md tells you to make before pushing ("Run
node scripts/check-nul-bytes.mjsbefore pushing") is the run most likely to be looking at a file that is new,
i.e. the one case the gate skips. So the pre-push check is green for a reason
unrelated to the bytes, on exactly the file most likely to carry one, since a
newly-authored file is where an editing tool materializes an escape.
Possible shapes (not a recommendation — this needs the gate owner)
--others --exclude-standardto thels-filescall so untracked,non-ignored files are scanned too, and say so in the gate's summary line.
("N untracked file(s) not scanned — stage them or pass
--all"), so thegreen is honest about its scope.
with the gate scoped to committed state by design.
Shape 1 changes what a bare local run means; shape 2 keeps the gate's contract
and closes the false-confidence half only. Either way the two-line self-scan in
AGENTS.md stays the belt to this gate's braces.
Refs #4890, #5140, #5157.