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:
- Index the path that was named. If it has no index, treat it as
init (or tell the user to run init).
- Keep the fallback but make it loud —
using the index at <ancestor> (…/child is not initialized) — so a wrong answer is at least visible.
- 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.
codegraph index <path>accepts an explicit path, then discards it whenever that path has no.codegraph/codegraph.dbof its own — walking up to the filesystem root and rebuilding the first initialized ancestor it finds. It prints a normalDoneand never says which project it actually indexed.Version 1.5.0, verified against tip
d6d1728.Repro (no git involved)
Actual:
Expected: either index the directory that was named, or refuse and say why. Not a silent substitution reported as success.
Cause
resolveProjectPathinsrc/bin/codegraph.ts:The walk has no stopping condition short of the filesystem root — no repo boundary, no
--force-style confirmation, no log line.findNearestCodeGraphRootinsrc/directory.tshas the same shape.Fall-back-to-ancestor is a reasonable default for a query run from a subdirectory (that is what
findNearestCodeGraphRootis for). It is a poor default forindex, 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:
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 worktreelayouts are especially exposed: sibling worktrees are never initialized themselves, so the walk escapes the repo on the firstindexin 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$HOMEor 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.tsalready 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 toindex.Suggested fix
For
indexspecifically, one of:init(or tell the user to runinit).using the index at <ancestor> (…/child is not initialized)— so a wrong answer is at least visible..gitpresent, 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 nevercodegraph index <path>. Happy to send a PR for whichever option you prefer.