Skip to content

index_repository silently skips all files reached via a directory symlink or NTFS junction #1815

Description

@N-Olbert

Version

codebase-memory-mcp 0.10.8

Platform

Windows (x64)

Install channel

GitHub release archive / install.sh / install.ps1

Binary variant

ui

What happened, and what did you expect?

On Windows (and probably also Linux), index_repository does not follow directory symlinks or NTFS junctions when walking the repo tree. Files reachable only through such a reparse point are completely absent from the resulting graph — they're not indexed, and they're also not reported in skipped, parse_partial, or not_indexed in the response or in index_status. From the tool's perspective, the directory (and everything under it) simply doesn't exist. This makes it impossible to index a codebase that uses linked/shared source directories without physically duplicating or hard-linking files.

Environment

  • OS: Windows 10/11
  • Filesystem: NTFS
  • Index mode tested: fast, moderate (same result in both)

Additional Information

  • Tested both a directory symlink and an NTFS junction (mklink /J) as the link type — both produce identical (null) results, so this isn't specific to one reparse-point type.
  • Tested indexing from multiple repo_path levels (the linked dir itself, its parent, and its grandparent) — result is the same regardless of which ancestor is passed as repo_path. The walker appears to prune the reparse point wherever it's encountered during the walk.
    • One exception: when the linked directory itself is passed as repo_path, it is resolved to its target directory and the target files are indexed successfully. As stated, the issue onlyaffects traversal through a reparse point below the selected repo_path.
  • As a control: replacing the directory symlink with a hard link of the individual file (New-Item -ItemType HardLink) — i.e. a real directory entry pointing at the same file content, not a reparse point — makes the file show up correctly in the graph. This confirms the walker specifically special-cases/skips reparse points (symlink and junction), rather than e.g. following a max-depth or .gitignore-driven exclusion.
  • .gitignore/.cbmignore are not the cause: git check-ignore on files behind the link correctly resolves per the gitignore rules — the exclusion is happening below the git layer, at the file-walk stage.

Impact

Any project layout using symlinked/junctioned shared source directories (common for legacy code shares, vendored/linked libraries, or monorepo cross-project sharing) gets silently and invisibly incomplete indexing. Because there's no coverage signal for it, it's easy to trust index_status's "clean" coverage report while a whole subtree is missing.

Suggested fix

At minimum, report skipped symlinks and junctions in index_status, for example:

{
  "path": "linked",
  "reason": "reparse-point"
}

Ideally, add an opt-in option to index_repository, such as:

"follow_symlinks": true

This should allow legitimate linked source directories to be traversed while keeping the current safe default behavior.

Reproduction

  1. Create a minimal repo layout ( I used the D-drive for fewer permission issues, you may adjust it to C:):
    D:\repro\
      legacy\
        shared\
          Bar.cs          <-- contains: public enum Bar { A, B }
      project\
        src\
          Foo.cs          <-- ordinary file, references Bar somehow or not, doesn't matter
        linked            <-- directory symlink OR NTFS junction -> D:\repro\legacy\shared
    
    Create the link with either:
    # symlink (needs elevation / Developer Mode)
    New-Item -ItemType SymbolicLink -Path D:\repro\project\linked -Target D:\repro\legacy\shared
    
    # OR junction (no elevation needed) — same result
    cmd /c mklink /J D:\repro\project\linked D:\repro\legacy\shared
Script to set up this repo
@echo off
set "ROOT=D:\repro"

mkdir "%ROOT%\legacy\shared" "%ROOT%\project\src" 2>nul

>"%ROOT%\legacy\shared\Bar.cs" echo namespace Shared;
>>"%ROOT%\legacy\shared\Bar.cs" echo.
>>"%ROOT%\legacy\shared\Bar.cs" echo public enum Bar { A, B }

>"%ROOT%\project\src\Foo.cs" echo namespace Project;
>>"%ROOT%\project\src\Foo.cs" echo.
>>"%ROOT%\project\src\Foo.cs" echo public static class Foo { }

if exist "%ROOT%\project\linked" (
    echo Link already exists: %ROOT%\project\linked
    exit /b 1
)

mklink /J "%ROOT%\project\linked" "%ROOT%\legacy\shared"

echo.
echo Repro created at %ROOT%\project   
  1. Index the repo:
    index_repository({ "repo_path": "D:/repro/project",  "name": "repro-junction",  "mode": "moderate",  "persistence": false })
    
  2. Inspect the result:
    search_graph({ "project": "repro-junction",  "name_pattern": ".*Bar.*",  "limit": 50,  "format": "json" })
    index_status(project="repro-junction")
    

Expected behavior

Either:

  • Bar.cs (reached via project/linked/Bar.cs) is indexed like a normal file, or
  • if symlinks are intentionally not followed, the file/directory should show up in index_status under not_indexed or skipped with a clear reason (e.g. "reason": "symlink" / "reason": "reparse-point"), so it's at least visible that content was excluded.

Actual behavior

  • search_graph returns zero results for anything under linked/.
  • index_status node/edge counts don't change whether the link exists or not.
  • linked/ and everything beneath it is absent from every coverage report (not_indexed, skipped, parse_partial) — there is no signal anywhere that content was excluded. It looks identical to "this directory doesn't exist," not "this directory was intentionally skipped."

In the reproduction, index_repository returned:

{
  "skipped_count": 0,
  "parse_partial_count": 0,
  "not_indexed_files_count": 0
}

index_status also returned empty lists for all three coverage categories.

Note that git itself follows the junction:

git add -A ; git status --short
A  linked/Bar.cs
A  src/Foo.cs
git check-ignore -v linked/Bar.cs → exit 1 (not ignored)

Logs

=== STDERR ===
Preparing one-shot local CBM command...
level=warn msg=mem.allocator.preloading_completed still_preloading=false detail=allocator_was_still_preloading,_so_arena_creation_and_purging_were_disabled;_process_init_completed_explicitly
level=info msg=mem.allocator.owned classes=all
level=info msg=mem.init budget_mb=11371 total_ram_mb=32489 source=ram_fraction
Running index_repository locally...
Completed index_repository (2749 ms)
=== STDOUT ===
{"project":"repro-logcapture","not_indexed_files_count":0,"skipped_count":0,"parse_partial_count":0,"nodes":6,"edges":5,"expected_nodes":6,"expected_edges":5,"adr_present":false,"adr_hint":"Project indexed. Consider creating an Architecture Decision Record: explore the codebase with get_architecture(aspects=['all']), then use manage_adr(mode='update') to persist architectural insights across sessions.","artifact_present":false,"status":"indexed"}

Diagnostics trajectory (memory / performance / leak issues)


Project scale (if relevant)

No response

Confirmations

  • I searched existing issues and this is not a duplicate.
  • My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.ux/behaviorDisplay bugs, docs, adoption UXwindowsWindows-specific issues

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions