fix(installer,cli): refuse to index $HOME / filesystem root (#860)

Running the installer or `codegraph init`/`index` from $HOME auto-indexed the
entire home tree (installer indexes process.cwd() with no guard), producing a
multi-GB ~/.codegraph/codegraph.db; the install dir sharing the ~/.codegraph
name then made every home subdir resolve its root to $HOME. On pre-1.0 macOS the
per-file watcher over that tree exhausted kern.maxfiles and crashed the machine
(#845; the fd blowup was fixed in 1.0.0, this fixes the root cause).

Add unsafeIndexRootReason() and refuse the home dir, a parent of home, and
filesystem roots at the installer auto-index, `init`, and `index`. Overridable
with --force. Closes #845.
This commit is contained in:
Colby Mchenry
2026-06-13 12:35:18 -05:00
committed by GitHub
parent 484da77296
commit 2472508549
5 changed files with 136 additions and 3 deletions
+47
View File
@@ -5,6 +5,7 @@
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
/** The default per-project data directory name. */
@@ -108,6 +109,52 @@ export function isInitialized(projectRoot: string): boolean {
* @param startPath - Directory to start searching from
* @returns The project root containing .codegraph/, or null if not found
*/
/**
* Reason a directory is unsafe to use as an index ROOT, or null when it's fine.
*
* Indexing your home directory or a filesystem root drags in caches, `Library`,
* every other project, etc. — a multi-GB index, constant file-watcher churn, and
* (pre-1.0 on macOS) a file-descriptor blowup that exhausted `kern.maxfiles` and
* took unrelated apps / the whole machine down (#845). The classic trigger:
* running the installer or `codegraph init` from `$HOME`, which auto-indexes the
* current directory. These are never intended project roots, so the installer
* and `init`/`index` refuse them (overridable with `--force`).
*
* Pure-ish (reads only `os.homedir()` + realpath) so it's easy to unit-test.
* The returned string is a human phrase that slots into "… looks like {reason}".
*/
export function unsafeIndexRootReason(projectRoot: string): string | null {
const resolve = (p: string): string => {
try {
return fs.realpathSync(path.resolve(p));
} catch {
return path.resolve(p);
}
};
const resolved = resolve(projectRoot);
// Filesystem root: `/` on POSIX, a drive root like `C:\` on Windows.
if (path.parse(resolved).root === resolved) {
return 'the filesystem root';
}
const home = resolve(os.homedir());
// Case-insensitive on macOS/Windows (case-preserving but case-insensitive FS).
const norm = (p: string): string =>
process.platform === 'darwin' || process.platform === 'win32' ? p.toLowerCase() : p;
const r = norm(resolved);
const h = norm(home);
if (r === h) {
return 'your home directory';
}
// An ancestor of home (e.g. `/Users`, `/home`) — even broader than home.
if (h.startsWith(r + path.sep)) {
return 'a parent of your home directory';
}
return null;
}
export function findNearestCodeGraphRoot(startPath: string): string | null {
let current = path.resolve(startPath);
const root = path.parse(current).root;