abhigyanpatwari/GitNexus · error · Error

Refusing symlinked auto-sync quarantine root: ${quarantineRo

Error message

Refusing symlinked auto-sync quarantine root: ${quarantineRoot}

What it means

pruneQuarantineEntries refuses to run when the quarantine root itself is a symlink (checked with lstat, which does not follow links). Because readdir/stat resolve through links, a symlinked root would cause the age-based prune to delete entries in some other directory entirely; this guard turns that silent data-loss risk into an explicit error naming the offending path.

Source

Thrown at gitnexus/src/core/auto-sync/path-security.ts:158

      'GitNexus auto-sync isolated a partial or unsafe clone result.',
      `Created at: ${new Date().toISOString()}`,
      `Original path: ${targetDir}`,
      `Retention: keep for ${QUARANTINE_RETENTION_DAYS} days unless an operator reviews and removes it earlier.`,
      'Cleanup: verify the original path and remote before manual deletion.',
      '',
    ].join('\n'),
    'utf-8',
  );
  return destination;
}

async function pruneQuarantineEntries(quarantineRoot: string): Promise<void> {
  const cutoff = Date.now() - QUARANTINE_RETENTION_DAYS * 24 * 60 * 60 * 1_000;
  // readdir and stat both resolve through a link, so a symlinked quarantine
  // root would age-sweep and delete entries somewhere else entirely.
  const rootStat = await fs.lstat(quarantineRoot).catch(() => undefined);
  if (rootStat?.isSymbolicLink()) {
    throw new Error(`Refusing symlinked auto-sync quarantine root: ${quarantineRoot}`);
  }
  let entries;
  try {
    entries = await fs.readdir(quarantineRoot);
  } catch (err: unknown) {
    if ((err as NodeJS.ErrnoException).code === 'ENOENT') return;
    throw err;
  }
  const survivors = (
    await Promise.all(
      entries
        .filter((entry) => entry.startsWith('auto-sync-'))
        .map(async (entry) => {
          const entryPath = path.join(quarantineRoot, entry);
          const stat = await fs.stat(entryPath).catch(() => undefined);
          if (stat && stat.mtimeMs < cutoff) {
            await fs.rm(entryPath, { recursive: true, force: true });
            return undefined;

View on GitHub (pinned to 0d1aed942f)

Solutions

  1. Replace the symlink with a real directory (move the data and remove the link) at the configured quarantine path.
  2. Update the auto-sync config to point local_path/quarantine settings at the real directory location.
  3. Verify with `ls -ld <path>` that the target is not a symlink (first character not 'l'), then re-run.

Example fix

// shell: before
ln -s /mnt/other/quarantine /var/lib/gitnexus/quarantine
// shell: after
mv /mnt/other/quarantine/* /var/lib/gitnexus/quarantine/ && rm /var/lib/gitnexus/quarantine-link; mkdir -p /var/lib/gitnexus/quarantine
Defensive patterns

Strategy: try-catch

Validate before calling

const st = await fs.lstat(quarantinePath).catch(() => null);
if (st?.isSymbolicLink()) throw new Error('quarantine path must be a real directory, not a symlink');

Type guard

function isRealDirStat(st: fs.Stats): boolean {
  return st.isDirectory() && !st.isSymbolicLink();
}

Try / catch

try {
  await resolveConfiguredCloneRoot(cfg);
} catch (err) {
  if (String(err.message).startsWith('Refusing symlinked auto-sync quarantine root')) {
    console.error('Replace the symlink with a real directory at ' + err.message.split(': ')[1]);
  } else throw err;
}

Prevention

When it happens

Trigger: resolveConfiguredCloneRoot triggers quarantine pruning while the configured quarantine directory is a symbolic link, e.g. /var/lib/gitnexus/quarantine -> /mnt/other-disk/quarantine.

Common situations: Admins symlinking the quarantine dir onto another volume, container images replacing the directory with a link, or a previous migration leaving a link behind.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-09-08). Data as JSON: /api/errors/af80e4340ae3b0fa. Report an issue: GitHub.