Skip to content

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

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.

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