Skip to content

codegraph index <path> silently ignores <path> and rebuilds an ancestor's index when <path> is not initialized #1524

Closed

Description

@inth3shadows

codegraph index <path> accepts an explicit path, then discards it whenever that path has no .codegraph/codegraph.db of its own — walking up to the filesystem root and rebuilding the first initialized ancestor it finds. It prints a normal Done and never says which project it actually indexed.

Version 1.5.0, verified against tip d6d1728.

Repro (no git involved)

#!/usr/bin/env bash
set -eu
T="$(mktemp -d)/reproindex"; mkdir -p "$T/parent/child"; cd "$T"
printf 'def parent_only():\n    return 1\n' > parent/p.py
printf 'def child_only():\n    return 2\n'  > parent/child/c.py

( cd parent && codegraph init . )                 # initialize ONLY the parent
( cd parent/child && codegraph index . )          # ask for the CHILD, explicitly

ls -d parent/child/.codegraph 2>/dev/null || echo "child has no index"
( cd parent/child && codegraph status | grep -i '^Project' )

Actual:

2. ask codegraph to index the CHILD, by explicit path
   ●  4 nodes, 2 edges in 363ms
   └  Done

3. what actually happened
   child/.codegraph created : NO  <-- the request was silently ignored
   'codegraph status' from the child reports:
     Project: /tmp/tmp.hpPpmKR0dq/reproindex/parent

Expected: either index the directory that was named, or refuse and say why. Not a silent substitution reported as success.

Cause

resolveProjectPath in src/bin/codegraph.ts:

function resolveProjectPath(pathArg?: string): string {
  const absolutePath = path.resolve(pathArg || process.cwd());
  if (isInitialized(absolutePath)) return absolutePath;

  // Walk up to find nearest parent with CodeGraph initialized
  let current = absolutePath;
  const root = path.parse(current).root;
  while (current !== root) {
    const parent = path.dirname(current);
    if (parent === current) break;
    current = parent;
    if (isInitialized(current)) { /* ...use the ancestor... */ }
  }

The walk has no stopping condition short of the filesystem root — no repo boundary, no --force-style confirmation, no log line. findNearestCodeGraphRoot in src/directory.ts has the same shape.

Fall-back-to-ancestor is a reasonable default for a query run from a subdirectory (that is what findNearestCodeGraphRoot is for). It is a poor default for index, whose entire job is to build an index at a location the caller named.

Why it matters in practice

The severity comes from how far the walk can travel. My repos live under a shared parent:

~/personal_projects/
├── testgraph/{.bare,main,...}     <- 40+ sibling repos
├── runecho/...
└── ...

Running codegraph index . from an un-indexed repo there walked up past the repo and built a 279 MB index of ~11,000 files across 55 unrelated projects at ~/personal_projects/.codegraph. Once that exists, every subsequent query from any un-indexed directory beneath it resolves to that index and answers from the wrong codebase — still silently. On one occasion the run was interrupted, leaving 880,880 unresolved references, so answers were not merely from the wrong project but truncated as well.

git worktree layouts are especially exposed: sibling worktrees are never initialized themselves, so the walk escapes the repo on the first index in a fresh worktree. But as the repro shows, git is not required — any un-indexed subdirectory does it.

The existing guard (Refusing to index … it looks like your home directory, #845) only catches the case where the walk reaches $HOME or a filesystem root. It does not fire for an intermediate ancestor, which is the common case.

Existing related work, which does not cover this

src/sync/worktree.ts already documents this walk as worktree-unaware, and PR #312 warns when a query borrows another worktree's index — but that is scoped to worktrees nested inside a checkout, applies to MCP queries, and does not apply to index.

Suggested fix

For index specifically, one of:

  1. Index the path that was named. If it has no index, treat it as init (or tell the user to run init).
  2. Keep the fallback but make it loud — using the index at <ancestor> (…/child is not initialized) — so a wrong answer is at least visible.
  3. Bound the walk at a repo boundary (.git present, worktree or not).

(1) matches what the argument means. (2) is the smallest change that removes the silence.

Note on discovery

Found while pointing the tool at a project of my own; the workaround is to use codegraph init <path> for a first index and never codegraph index <path>. Happy to send a PR for whichever option you prefer.

Activity

  1. wstczyw commented on Aug 11, 2026

    @wstczyw

    I reproduced this against current main at
    c6aaa20358cd6adcd04b87bdef8e5803ad146f3a, using a generic
    indexed parent directory containing an independent, unindexed nested Git
    repository.

    One additional concern from source inspection: resolveProjectPath() is also
    used by sync, uninit, unlock, status, and the read/query commands.
    Therefore this is broader than index: an explicit child path can select an
    ancestor for both read and mutation operations. In particular, mutation
    commands should not silently act on an ancestor selected for an explicit path.

    A fail-closed workaround for automation is:

    1. Resolve the Git root owning the target:
      git -C <target> rev-parse --show-toplevel
    2. Run codegraph status <git-root> --json.
    3. Normalize both paths and require projectPath === git-root.
    4. On mismatch, treat the target as unindexed and do not query, sync, index,
      unlock, or uninitialize the ancestor.

    For an upstream fix, one possible command policy is:

    • init <path>: exact path, as today.
    • Mutating commands with an explicit path (index, sync, uninit,
      unlock): require an exact initialized project or fail while reporting the
      detected ancestor.
    • Read commands: retain nearest-ancestor behavior only within the same detected
      VCS root.
    • status --json: expose requestedPath, projectPath, and a resolution value
      such as exact, ancestor, or uninitialized.

    Useful regression cases would cover:

    • an explicit unindexed child never mutates its indexed ancestor;
    • a normal subdirectory inside one repository still resolves its repository
      index;
    • an independent nested Git repository never borrows the outer repository;
    • JSON output makes every ancestor resolution explicit.

    This avoids requiring Git for all projects while making explicit paths and
    mutation commands fail closed.

  2. inth3shadows commented on Sep 4, 2026

    @inth3shadows
    ContributorAuthor

    Status: still reproduces on 1.6.0 (npm latest), unchanged from the report. The ## Suggested fix above still applies as written.

    parent/                  <- has .codegraph (2 nodes)
    parent/p.py                  def parent_fn()
    parent/child/            <- NO .codegraph
    parent/child/src/c.py        def child_fn()
    
    $ codegraph index /abs/path/parent/child
    ◆  Indexed 2 files
    ●  4 nodes, 2 edges in 314ms
    └  Done
    

    Two files, when child holds exactly one. Afterwards:

    • parent/child/.codegraph — does not exist; the explicitly named path never got an index.
    • parent/.codegraph now holds both:
    name file_path
    parent_fn p.py
    child_fn child/src/c.py

    So the ancestor index was silently rebuilt and widened to absorb the child, the named path was ignored, and the command reported success. "Indexed 2 files" is the only hint, and only if you already know the target's file count.

    Verified on 1.6.0, isolated fixture outside any real project tree.

  3. colbymchenry commented on Sep 8, 2026

    @colbymchenry
    Owner

    Fix landed (Linux verified)

    PR: #1797
    Commit: 327ea5aad5cfbba8a3cddeffffd1e7a88650bb3e
    Upstream used: #1689 by @danusha2345 (cherry-picked; author preserved)

    Root cause

    codegraph index <path> called resolveProjectPath, which walks upward to the nearest initialized ancestor when <path> has no index — so an explicit child path silently rebuilt the parent and reported Done.

    Fix

    An explicit path names the project to rebuild and is not a hint to look elsewhere. Uninitialized explicit paths exit 1, name the ancestor that would have been rebuilt, and point at codegraph init for the named path. Bare codegraph index (cwd) still resolves upward.

    Linux verify (fail → pass)

    On tip before the fix (374b3b42): init parent only → index . from child → Done; child has no .codegraph; status from child reports the parent.

    After the fix: same repro exits 1 with:
    CodeGraph not initialized in …/child and The nearest initialized project is …/parent …; child still has no .codegraph; parent DB mtime unchanged.

    Focused suite: npx vitest run __tests__/cli-index-explicit-path.test.ts — 2 passed.

    Closing this issue as completed; #1689 will be closed as superseded by #1797.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions