Skip to content

cdidx <path> silently indexes 0 files when <path> itself is a SkipDirs-named directory (node_modules, target, vendor, bin, dist, ...) #153

Description

@Widthdom

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions