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
- Ensure the target's parent directory physically resides inside the clone root (no symlinks in the parent chain).
- Resolve the clone root once with fs.realpath and build targetDir from the resolved root and repo name.
- Remove or replace the offending symlink with a real directory.
- 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
- fs.realpath the clone root once at startup and build all targets from it.
- Avoid symlinked directories anywhere in the clone hierarchy.
- On macOS, remember /tmp is a symlink to /private/tmp — realpath roots before use.
- Treat unexpected symlinks in the clone tree as a security incident.
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
- Clone target 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
- path must include owner/repo without traversal
AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-09-08).
Data as JSON: /api/errors/6e4d064e7d3b62a9.
Report an issue: GitHub.