abhigyanpatwari/GitNexus · error
Refusing to delete : resolved path is outside storage…
Error message
Refusing to delete ${dbPath}: resolved path ${realPath} is outside storage directory What it means
A safety guard in lbug-adapter.ts (line 832): when the path to clear is a directory (old-style directory database or leftover), its realpath must resolve inside the realpath'd parent storage directory before `fs.rm(..., {recursive, force})` runs. Symlinked dbPaths are never followed — only the link is unlinked. If realpath resolves outside the parent (symlink chains, bind mounts), deletion is refused; this prevents a crafted or accidentally-symlinked layout from deleting arbitrary directories.
Solutions
- Inspect the path from the message: run `readlink -f <dbPath>` and compare with `readlink -f <parentDir>`
- If the target is genuinely the old directory database, remove it manually (`rm -rf <realPath>` after verifying), then re-run so a fresh database is created
- Avoid symlinking the database path across filesystem boundaries; symlink the whole storage directory instead, so parent and child resolve consistently
Example fix
# before: db path is a symlink pointing elsewhere $ ln -s /mnt/other/db ~/.gitnexus/repo/db # after: remove manually, let GitNexus recreate in-place $ rm -rf /mnt/other/db gitnexus analyze --force /path/to/repo
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:path';
import path from 'node:path';
// Before instructing any cleanup, ensure dbPath resolves inside its parent
const realPath = await fs.realpath(dbPath);
const realParent = await fs.realpath(path.dirname(dbPath));
if (!realPath.startsWith(realParent + path.sep) && realPath !== realParent) {
throw new Error(`Refusing cleanup: ${dbPath} resolves to ${realPath}, outside ${realParent} — remove manually if intended`);
} Try / catch
try {
await prepareDbPath(dbPath);
} catch (err) {
if (err instanceof Error && err.message.startsWith('Refusing to delete')) {
// inspect with `readlink -f`; if the target is genuinely the old db, remove it manually and re-run
}
throw err;
} Prevention
- Do not symlink individual database files out of the storage dir; relocate the whole storage directory instead
- When moving ~/.gitnexus, move it atomically (rename) rather than leaving symlinks behind
- Treat this refusal as a safety tripwire — always resolve it by inspection, never by forcing deletion flags
When it happens
Trigger: dbPath is a symlink whose target directory lives outside the storage dir (e.g. user symlinked ~/.gitnexus/…/db to another disk, but here it points at a real directory); or mount/realpath weirdness (bind mounts, container volumes) makes the target resolve to a path not prefixed by the parent's realpath.
Common situations: Users relocating GitNexus storage to another drive via symlinks; migrating ~/.gitnexus between machines with partial symlinking; exotic container volume layouts where parent and child resolve differently.
Related errors
- Clone failed and partial checkout could not be quarantined
- Clone target must be a subdirectory of
- Could not remove the shadowed branch sub-index; keeping its…
- Guard and workload-lock cleanup failed
- Path traversal denied
AI-assisted analysis of abhigyanpatwari/GitNexus@ac9a4e9abd (2026-08-20).
Data as JSON: /api/errors/69a6c617b4f6353c.
Report an issue: GitHub.
Appendix: source
Thrown at gitnexus/src/core/lbug/lbug-adapter.ts:896
conn = usable.conn;
currentDbReadOnly = true;
} else {
// LadybugDB stores the database as a single file (not a directory).
// If the path already exists, it must be a valid LadybugDB database file.
// Remove stale empty directories or files from older versions.
try {
const stat = await fs.lstat(dbPath);
if (stat.isSymbolicLink()) {
// Never follow symlinks — just remove the link itself
await fs.unlink(dbPath);
} else if (stat.isDirectory()) {
// Verify path is within expected storage directory before deleting
const realPath = await fs.realpath(dbPath);
const parentDir = path.dirname(dbPath);
const realParent = await fs.realpath(parentDir);
const safePrefix = realParent.endsWith(path.sep) ? realParent : realParent + path.sep;
if (!realPath.startsWith(safePrefix) && realPath !== realParent) {
throw new Error(
`Refusing to delete ${dbPath}: resolved path ${realPath} is outside storage directory`,
);
}
// Old-style directory database or empty leftover - remove it
await fs.rm(dbPath, { recursive: true, force: true });
}
// If it's a file, assume it's an existing LadybugDB database - LadybugDB will open it
} catch (err) {
if (!isMissingFileError(err)) {
throw err;
}
// Path doesn't exist, which is what LadybugDB wants for a new database
}
// -------------------------------------------------------------------------
// Cross-process critical section: acquire init lock, clean orphan sidecars,
// and open the database. The lock prevents a TOCTOU race where another
// process could create a fresh DB between our access() check and theView on GitHub (pinned to ac9a4e9abd)