abhigyanpatwari/GitNexus · error · Error

Clone target parent must resolve inside ${root}

Error message

Clone target parent must resolve inside ${root}

What it means

assertPreRealpathContainment resolves the clone root and the target's parent directory to their real filesystem paths (following symlinks) and requires the parent to resolve inside the real root. This blocks symlink-based escapes — e.g. a parent directory that is a symlink pointing outside the clone root — before any clone/pull happens.

Source

Thrown at gitnexus/src/server/git-clone.ts:493

              [err, quarantineError],
              `Clone failed and partial checkout could not be quarantined: ${safeTarget}`,
            );
          }
        }
      }
      throw err;
    }
  }

  return safeTarget;
}

async function assertPreRealpathContainment(root: string, target: string): Promise<void> {
  const realRoot = await fs.realpath(root);
  const realParent = await fs.realpath(path.dirname(target));
  const parentRel = path.relative(realRoot, realParent);
  if (parentRel.startsWith('..') || path.isAbsolute(parentRel)) {
    throw new Error(`Clone target parent must resolve inside ${root}`);
  }
}

async function assertPostRealpathContainment(root: string, target: string): Promise<void> {
  const realRoot = await fs.realpath(root);
  const realTarget = await fs.realpath(target);
  const rel = path.relative(realRoot, realTarget);
  if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
    throw new Error(`Clone target must resolve inside ${root}`);
  }
}

async function assertNoSymlinkPath(
  root: string,
  target: string,
  verifyOwnership = false,
): Promise<void> {
  const resolvedRoot = path.resolve(root);

View on GitHub (pinned to 0d1aed942f)

Solutions

  1. Ensure the target's parent directory physically resides inside the clone root (no symlinks in the parent chain).
  2. Resolve the clone root once with fs.realpath and build targetDir from the resolved root and repo name.
  3. Remove or replace the offending symlink with a real directory.
  4. If the root itself is a symlink (e.g. /tmp -> /private/tmp), pass the realpath of the root as allowedCloneRoot.

Example fix

// before
await cloneOrPull({ url, targetDir: '/tmp/clones/repo' }); // /tmp is a symlink
// after
const root = await fs.realpath('/tmp/clones');
await cloneOrPull({ url, targetDir: path.join(root, 'repo'), allowedCloneRoot: root });
Defensive patterns

Strategy: validation

Validate before calling

import { fs } from 'node:fs/promises';
const realRoot = await fs.realpath(allowedCloneRoot);
const realParent = await fs.realpath(path.dirname(targetDir));
if (path.relative(realRoot, realParent).startsWith('..')) {
  throw new Error(`parent of ${targetDir} escapes clone root ${realRoot}`);
}

Try / catch

try {
  await cloneOrPull(opts);
} catch (err) {
  if ((err as Error).message.startsWith('Clone target parent must resolve inside')) {
    throw new Error(`Symlinked parent in ${opts.targetDir}; use a path under the real clone root.`, { cause: err });
  }
  throw err;
}

Prevention

When it happens

Trigger: targetDir's parent is a symlink to a location outside the clone root (fs.realpath of parent escapes realRoot); cloneRoot itself is a symlink resolving elsewhere while target's parent resolves outside; an attacker-planted symlink in the clone path hierarchy.

Common situations: macOS /tmp being a symlink to /private/tmp without normalizing the root; user-configured clone roots containing symlinked subdirectories; containers where /var symlinks to another mount; malicious setups attempting path escape via symlinks.

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/6e4d064e7d3b62a9. Report an issue: GitHub.