Summary
FileIndexer.ScanDirectory applies the SkipDirs check to the project root itself, not only to descendant directories encountered during recursion. When a user explicitly points cdidx at a directory whose leaf name matches any entry in SkipDirs (node_modules, target, vendor, bin, dist, build, out, .gradle, .venv, coverage, etc.), the scan aborts at the root and produces an empty index with no warning.
This is different from the intended skip-descendants behavior: the skip list exists so a normal cdidx myproject won't crawl into dependency / build caches nested inside the project. When the user explicitly says "index this one", the tool should honor that.
Repro
On this environment there is a real node_modules tree at /opt/node22/lib/node_modules (223 MB of actual JS sources, npm, eslint, prettier, playwright, etc., not a fake empty dir):
curl -fsSL https://raw.githubusercontent.com/Widthdom/CodeIndex/main/install.sh | bash
CDIDX=/root/.local/bin/cdidx
# Explicitly target the node_modules root
"$CDIDX" /opt/node22/lib/node_modules --db /tmp/node_mods.db --verbose
Observed
Project : /opt/node22/lib/node_modules
Output : /tmp/node_mods.db
Mode : incremental
Scanning...
Found 0 files
...
Done.
Files : 0
Chunks : 0
Symbols : 0
Refs : 0
Elapsed : 00:00:00
"$CDIDX" status --db /tmp/node_mods.db then confirms 0 files, 0 symbols, 0 refs across 0 languages (); index unknown.
Contrast: targeting /opt/node22/lib/node_modules/npm (one level deeper, whose leaf name npm is not in SkipDirs) indexes 283 files (111 JavaScript, 85 HTML, 84 Markdown, etc.) in ~3 s.
Same class of failure reproduces for any SkipDirs entry as the leaf, e.g. cdidx target/ inside a Rust/Java project, cdidx vendor/ inside a Go project, cdidx bin/ or cdidx dist/ if the user is reviewing build output, or cdidx . run from inside such a directory (because Path.GetFileName on a normalized . returns the leaf of CWD).
Root cause
src/CodeIndex/Indexer/FileIndexer.cs:176-181:
private void ScanDirectory(string dir, List<string> results)
{
// Check for skip directories / スキップ対象ディレクトリかチェック
var dirName = Path.GetFileName(dir);
if (SkipDirs.Contains(dirName))
return;
...
}
Callee of ScanFiles() at line 169-174:
public List<string> ScanFiles()
{
var files = new List<string>();
ScanDirectory(_projectRoot, files);
return files;
}
ScanDirectory does not distinguish the initial project-root call from recursive descent calls, so the skip check applies to the root. SkipDirs currently contains:
.git, .svn, .hg,
node_modules, __pycache__, .pytest_cache,
venv, .venv, env,
dist, build, .build, out,
bin, obj, target, .gradle,
.next, .nuxt,
.idea, .vscode,
coverage, vendor,
.terraform, .cargo, .pub-cache, _build
Any of these as the leaf of _projectRoot will silently short-circuit the scan.
Why it matters
- Silent empty indexes. The output looks like a successful, instant run (
Elapsed : 00:00:00, no error, exit 0). A human might only notice when a subsequent search / symbols / status returns nothing. An AI agent that trusts exit codes will just move on.
- Legitimate use cases are blocked. Reviewing dependency source in
node_modules / vendor, inspecting build output in target / dist / out, debugging a .venv scaffold, or just "cdidx ." from inside any of these directories are all reasonable things to try.
- Asymmetric with other guards. If the scan finds the project root but zero indexable files (e.g. an empty dir or a dir with only binary blobs), the user still gets "Found 0 files" — same observable output, different root cause. No way to tell the two apart from the CLI.
Suggested direction
Keep the skip-list behavior for descendant dirs (that's the point), but special-case the root so explicit intent wins. Two reasonable shapes:
Option A — always scan the root, skip on recursion
Split ScanDirectory so the first call bypasses SkipDirs:
public List<string> ScanFiles()
{
var files = new List<string>();
EnumerateDirectory(_projectRoot, files); // never skip the root
return files;
}
private void ScanDirectory(string dir, List<string> results)
{
var dirName = Path.GetFileName(dir);
if (SkipDirs.Contains(dirName)) return;
EnumerateDirectory(dir, results);
}
private void EnumerateDirectory(string dir, List<string> results)
{
// existing enumerate-files + recurse-into-subdirs-via-ScanDirectory body
}
Path.GetFullPath(_projectRoot) resolved once in the constructor gives a stable comparison anchor if you prefer comparing absolute paths instead of splitting methods.
Option B — WARN and continue when the root is a SkipDirs name
Print a stderr note ("node_modules is in the default skip list but you targeted it explicitly; indexing anyway"), then scan. Keeps users informed and still useful for the common case. Option A + Option B combined works too: scan the root unconditionally, emit the WARN only when the root's leaf is in SkipDirs so the user knows why they're getting a warning on first descent.
Option C — at minimum, fail loudly instead of silently
If the decision is to keep blocking the root (unlikely the right call, but documented), convert "Found 0 files" into an explicit WARN / non-zero exit when the reason is SkipDirs membership, not "the directory is actually empty / has only unknown extensions". That at least makes the behavior observable.
My preference: Option A + a one-line WARN so behavior is correct and discoverable.
Scope
src/CodeIndex/Indexer/FileIndexer.cs — split scan entry point.
- Regression tests in
tests/CodeIndex.Tests/FileIndexerTests.cs — target node_modules / target / vendor / bin as the project root and assert files are found. Keep existing tests that assert descendant-skip behavior.
- Optional: update help / README to clarify that the skip list applies to nested dirs, not to an explicit target.
Environment
- cdidx: v1.9.0 (installed via
install.sh)
- Targeted:
/opt/node22/lib/node_modules (223 MB, real npm + eslint + prettier + playwright sources, not a fake fixture)
- Control pass:
/opt/node22/lib/node_modules/npm — leaf name npm — indexes 283 files in 3 s.
- Platform: linux-x64 container.
- Filed from a cloud Claude Code session per
CLOUD_BOOTSTRAP_PROMPT.md.
Summary
FileIndexer.ScanDirectoryapplies theSkipDirscheck to the project root itself, not only to descendant directories encountered during recursion. When a user explicitly points cdidx at a directory whose leaf name matches any entry inSkipDirs(node_modules,target,vendor,bin,dist,build,out,.gradle,.venv,coverage, etc.), the scan aborts at the root and produces an empty index with no warning.This is different from the intended skip-descendants behavior: the skip list exists so a normal
cdidx myprojectwon't crawl into dependency / build caches nested inside the project. When the user explicitly says "index this one", the tool should honor that.Repro
On this environment there is a real
node_modulestree at/opt/node22/lib/node_modules(223 MB of actual JS sources, npm, eslint, prettier, playwright, etc., not a fake empty dir):Observed
"$CDIDX" status --db /tmp/node_mods.dbthen confirms0 files, 0 symbols, 0 refs across 0 languages (); index unknown.Contrast: targeting
/opt/node22/lib/node_modules/npm(one level deeper, whose leaf namenpmis not inSkipDirs) indexes 283 files (111 JavaScript, 85 HTML, 84 Markdown, etc.) in ~3 s.Same class of failure reproduces for any
SkipDirsentry as the leaf, e.g.cdidx target/inside a Rust/Java project,cdidx vendor/inside a Go project,cdidx bin/orcdidx dist/if the user is reviewing build output, orcdidx .run from inside such a directory (becausePath.GetFileNameon a normalized.returns the leaf of CWD).Root cause
src/CodeIndex/Indexer/FileIndexer.cs:176-181:Callee of
ScanFiles()at line 169-174:ScanDirectorydoes not distinguish the initial project-root call from recursive descent calls, so the skip check applies to the root.SkipDirscurrently contains:Any of these as the leaf of
_projectRootwill silently short-circuit the scan.Why it matters
Elapsed : 00:00:00, no error, exit 0). A human might only notice when a subsequentsearch/symbols/statusreturns nothing. An AI agent that trusts exit codes will just move on.node_modules/vendor, inspecting build output intarget/dist/out, debugging a.venvscaffold, or just "cdidx ." from inside any of these directories are all reasonable things to try.Suggested direction
Keep the skip-list behavior for descendant dirs (that's the point), but special-case the root so explicit intent wins. Two reasonable shapes:
Option A — always scan the root, skip on recursion
Split
ScanDirectoryso the first call bypassesSkipDirs:Path.GetFullPath(_projectRoot)resolved once in the constructor gives a stable comparison anchor if you prefer comparing absolute paths instead of splitting methods.Option B — WARN and continue when the root is a
SkipDirsnamePrint a stderr note ("
node_modulesis in the default skip list but you targeted it explicitly; indexing anyway"), then scan. Keeps users informed and still useful for the common case. Option A + Option B combined works too: scan the root unconditionally, emit the WARN only when the root's leaf is inSkipDirsso the user knows why they're getting a warning on first descent.Option C — at minimum, fail loudly instead of silently
If the decision is to keep blocking the root (unlikely the right call, but documented), convert "Found 0 files" into an explicit WARN / non-zero exit when the reason is
SkipDirsmembership, not "the directory is actually empty / has only unknown extensions". That at least makes the behavior observable.My preference: Option A + a one-line WARN so behavior is correct and discoverable.
Scope
src/CodeIndex/Indexer/FileIndexer.cs— split scan entry point.tests/CodeIndex.Tests/FileIndexerTests.cs— targetnode_modules/target/vendor/binas the project root and assert files are found. Keep existing tests that assert descendant-skip behavior.Environment
install.sh)/opt/node22/lib/node_modules(223 MB, real npm + eslint + prettier + playwright sources, not a fake fixture)/opt/node22/lib/node_modules/npm— leaf namenpm— indexes 283 files in 3 s.CLOUD_BOOTSTRAP_PROMPT.md.