abhigyanpatwari/GitNexus · error · Error
Clone target must resolve inside ${root}
Error message
Clone target must resolve inside ${root} What it means
assertPostRealpathContainment runs after clone/pull and requires the fully resolved target directory to be strictly inside the resolved clone root (empty relative path, '..', or absolute all fail). Unlike the parent-only pre-check, this catches targets that were swapped or symlinked during the operation (TOCTOU) and confirms the final checkout really landed under the root.
Source
Thrown at gitnexus/src/server/git-clone.ts:502
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);
const resolvedTarget = path.resolve(target);
const relativeTarget = path.relative(resolvedRoot, resolvedTarget);
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) return;
let current = resolvedRoot;
for (const segment of relativeTarget.split(path.sep).filter(Boolean)) {
current = path.join(current, segment);
let stat;
try {
stat = await fs.lstat(current);View on GitHub (pinned to 0d1aed942f)
Solutions
- Ensure no process replaces or symlinks the target directory during the clone/pull.
- Re-check the final path: it must resolve strictly under the clone root; recreate it as a real directory if it is a symlink.
- Re-run the operation after removing any symlink at the target path.
- Serialize access to the clone root (avoid concurrent clones to the same target).
Example fix
// before
await cloneOrPull({ url, targetDir: maybeSymlinkedPath });
// after
const st = await fs.lstat(maybeSymlinkedPath);
if (st.isSymbolicLink()) throw new Error('remove symlink first');
await cloneOrPull({ url, targetDir: maybeSymlinkedPath }); Defensive patterns
Strategy: validation
Validate before calling
import { fs } from 'node:fs/promises';
const realRoot = await fs.realpath(root);
let realTarget = target;
try { realTarget = await fs.realpath(target); } catch { /* not created yet: OK pre-clone */ }
if (!realTarget.startsWith(realRoot + path.sep)) {
throw new Error(`target resolves outside clone root`);
} Try / catch
try {
await cloneOrPull(opts);
} catch (err) {
if ((err as Error).message.startsWith('Clone target must resolve inside')) {
throw new Error(`Target at ${opts.targetDir} was swapped or symlinked during sync; inspect the directory.`, { cause: err });
}
throw err;
} Prevention
- Don't run concurrent jobs that mutate or relink the clone tree.
- Re-verify the final checkout path after any long-running clone.
- Keep the clone root on a filesystem you control exclusively.
- Re-run the operation after removing any symlink at the target path.
When it happens
Trigger: The target itself becomes a symlink during the operation; a race swaps the directory between pre-check and post-check; targetDir resolves to the clone root itself; nested symlink introduced mid-clone.
Common situations: Concurrent processes manipulating the clone tree; malicious or buggy tooling replacing directories during long clones; verification-style setups running the function on paths that are later linked elsewhere.
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
- Clone target parent must resolve inside ${root}
- Refusing symlink in clone target path: ${current}
- ${TRUSTED_CACHE_DIRECTORY_ENV} must not traverse symbolic li
- ${filename} must not be a symbolic link
- ${filename} moved or was replaced while being opened
AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-09-08).
Data as JSON: /api/errors/9cd1057773da3bdc.
Report an issue: GitHub.