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

  1. Inspect the path from the message: run `readlink -f <dbPath>` and compare with `readlink -f <parentDir>`
  2. 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
  3. 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

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


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 the

View on GitHub (pinned to ac9a4e9abd)