Skip to main content
Zhimalab
中文

The 85-Byte `nul` File That Broke an Entire Codebase Index

2026-08-27 · 14 min

Background

Running index this project on C:\codes\zhimalab (a full codebase-memory-mcp index) only returned a generic failure, with the real cause completely hidden. It took a long time to pin down, and the root cause turned out to be an 85-byte stray file named nul that had never been committed to git. This post isn’t about how to index — it’s about why a generic error message can send you on such a long detour.

Symptom: an error you can’t locate from

{"project":"C-codes-zhimalab","status":"error",
 "hint":"Pipeline failed. Check repo_path exists and contains source files."}

“Path doesn’t exist / no source files”? The repo is right there, with 1,265 files under src/. That hint is a catch-all — every pipeline error looks like this, and the real error is swallowed. I tried CBM_LOG_LEVEL=trace, CBM_INDEX_LOG, and CBM_LSP_DISABLED — none of them surfaced the detail (the worker exited cleanly with code 0, and the logs only ever showed the same generic error).

Investigation: two wrong turns

Wrong turn #1: suspecting the broken cache tree

The session’s cbm daemon was running against a corrupted cache tree at C:\c\Users\peini\.cache\codebase-memory-mcp (leftover from a botched --dir install), and the main zhimalab.db was missing from the formal cache. My first instinct was to blame it. But olympic indexed fine in the same cache → ruled out.

Wrong turn #2: C:\tmp’s DACL interference

To reproduce, I created a minimal TS repo and a minimal Python repo under C:\tmpboth failed, leading me to the wrong conclusion that “new projects always fail; only already-indexed ones succeed via incremental”. Only when I put test repos under C:\codes and both succeeded on the first try did I realize: C:\tmp has an insecure DACL, so test repos placed there get rejected by the cache-private check — entirely unrelated to the problem I was chasing.

The real isolation: clean clone + bisection

The key step was git clone to a clean path (under C:\codes, avoiding C:\tmp) and indexing that:

  • Committed content (clean clone) → success (7,817 nodes) → the problem isn’t in what’s committed;
  • Added the working-tree differences back one by one: the modified .cbmignore → success; the staged rename → success;
  • Added the four stray root files (nul, {G.difficulty, {G.topic, tmpclaude-c878-cwd) back → failure;
  • Bisected → only nul triggers the failure; the other three are harmless.

Root cause: a Windows reserved device name

nul is a Windows reserved device name (like con, prn, aux). When the indexer does open("C:/codes/zhimalab/nul"), Windows treats it as the null device rather than a normal file, the pipeline read fails, and the whole index fails.

The nasty part: .cbmignore already had a nul exclusion rule, and it couldn’t help — file enumeration/opening happens before ignore filtering, so by the time the rule could apply, the pipeline had already crashed. And the error was conveniently wrapped as “the repo has no source files”, which made it baffling.

The file’s content was a stray ls error captured by a misdirected redirect:

ls: cannot access 'C:codeszhimalabsrccomponentstutorials': No such file or directory

Pure accidental junk, never committed to git.

Fix

rm -f nul        # delete the stray file

Re-ran the full index: 27 seconds, 13,624 nodes / 45,794 edges, status: ready.

Retrospective: how to nail this fast next time

  1. Don’t take the generic hint literally. “repo_path exists and contains source files” is a catch-all; the tool swallows the real error.
  2. First thing, scan for reserved device names: ls -la the repo root for nul/con/prn/aux/com1-type files, and delete any found — this is the #1 known pitfall for indexing on Windows.
  3. Don’t put test repos under C:\tmp: its insecure DACL reliably fabricates a “new projects fail” illusion. Use C:\codes or your home directory.
  4. The fastest isolation is git clone to a clean path: in one step it separates “committed content” from “working-tree state” — stop guessing at logs.
  5. Suspect untracked stray files first: they’re not in git so they’re the easiest to overlook, and that’s exactly where this culprit was hiding.
  6. CBM_LOG_LEVEL / CBM_INDEX_LOG / CBM_LSP_DISABLED won’t rescue you from this class of error — don’t waste time on them.

Finally, a tip: turn “scan for stray files before indexing” into a one-command script (scan + delete + run the index) so index this project becomes a single command. This repo now has cbm-index.sh, which scans and deletes reserved-device-name strays, then runs the index — with built-in retry for conflicts with the daemon’s background auto-index. One implementation detail worth noting: this kind of per-file scan must use pure bash builtins (${f##*/}, case) — per-file $(basename)/grep hangs after a few hundred iterations on Git Bash due to subprocess accumulation.