stablyai/orca · critical
Clone path must be inside the destination directory
Error message
Clone path must be inside the destination directory
What it means
deriveValidatedClonePath joins the destination with the derived repo name, resolves both, computes the relative path from the destination to the resolved clone path, and throws if that relative path escapes the destination (is '', '..', starts with '..<sep>', or is absolute). This is a security guard: a crafted URL whose repo name resolves outside the destination must not produce a clone path that a later rm could walk outside the intended folder.
Source
Thrown at src/main/git/repo-clone-path.ts:53
!isAbsolute(args.destination) ||
(process.platform !== 'win32' && isWindowsAbsolutePathLike(args.destination))
) {
throw new Error('Clone destination must be an absolute path')
}
const repoName = deriveCloneRepoNameFromUrl(args.url)
const clonePath = join(args.destination, repoName)
const resolvedDestination = resolve(args.destination)
const resolvedClonePath = resolve(clonePath)
const pathFromDestination = relative(resolvedDestination, resolvedClonePath)
if (
pathFromDestination === '' ||
pathFromDestination === '..' ||
pathFromDestination.startsWith(`..${sep}`) ||
isAbsolute(pathFromDestination)
) {
throw new Error('Clone path must be inside the destination directory')
}
return clonePath
}
export function getClonePathComparisonKey(clonePath: string): string {
const resolvedClonePath = isWindowsAbsolutePathLike(clonePath) ? clonePath : resolve(clonePath)
const normalized = normalizeRuntimePathSeparators(resolvedClonePath)
const wslUncMatch = normalized.match(/^\/\/(?:wsl\.localhost|wsl\$)\/([^/]+)(\/.*)?$/i)
if (wslUncMatch) {
// Why: WSL UNC paths cross into a case-sensitive Linux filesystem, so only
// the Windows UNC server alias and distro segment should be case-folded.
const linuxPath = (wslUncMatch[2] ?? '').replace(/\/+$/, '')
return `//wsl/${wslUncMatch[1].toLowerCase()}${linuxPath}`
}
return normalizeRuntimePathForComparison(resolvedClonePath)
}
View on GitHub (pinned to 1136503c6a)
Solutions
- Use a real, non-symlinked directory as the clone destination; symlinked destinations can resolve outside themselves.
- Do not attempt to 'fix' the URL to dodge this guard — it is the last line preventing filesystem escape; fix the destination instead.
- On Windows, pass native Windows paths and avoid MSYS/Git Bash path rewriting by using forward-slash absolute paths from the main process.
- Run the clone under a dedicated, freshly-created base directory so resolution cannot cross into user data.
Example fix
// before
deriveValidatedClonePath({ url, destination: symlinkedDir })
// after: resolve symlinks first, use a concrete directory
import { realpath } from 'node:fs/promises'
const realDest = await realpath(baseCloneDir)
deriveValidatedClonePath({ url, destination: realDest }) Defensive patterns
Strategy: validation
Validate before calling
import { realpath } from 'node:fs/promises'
import { relative, resolve, sep, isAbsolute } from 'node:path'
async function clonePathStaysInsideDestination(url: string, destination: string): Promise<boolean> {
const realDest = await realpath(destination)
const repoName = deriveCloneRepoNameFromUrl(url) // assume :24/:27 already pass
const resolvedClone = resolve(realDest, repoName)
const rel = relative(realDest, resolvedClone)
return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)
} Type guard
function isClonePathOutsideDestination(error: unknown): boolean {
return error instanceof Error && error.message === 'Clone path must be inside the destination directory'
} Try / catch
const realDest = await realpath(destination)
if (!(await clonePathStaysInsideDestination(url, realDest))) {
throw new Error('Refusing to clone: resolved path escapes the destination. Use a non-symlinked folder.')
}
try { return deriveValidatedClonePath({ url, destination: realDest }) }
catch (error) { if (isClonePathOutsideDestination(error)) throw new Error('Destination resolves outside itself; pick a concrete folder.'); else throw error } Prevention
- Use a real, non-symlinked directory as the clone destination; realpath() it first.
- Do not attempt to craft URLs that dodge this guard — it is the last line preventing filesystem escape.
- On Windows, pass native Windows paths and avoid MSYS path rewriting.
When it happens
Trigger: A URL whose derived repo name, after join+resolve, ends up outside args.destination — e.g. a destination that is itself a symlinked directory whose target resolves elsewhere, an adversarial URL that survives the :24/:27 guards but still moves the resolved path, or a junction/MSYS path on Windows whose resolution crosses drive roots.
Common situations: Cloning into a destination that is a symlink to another volume (resolve() follows symlinks, so the relative path can become absolute or '..'-prefixed); Windows MSYS path rewriting mangling the destination before it reaches the main process; adversarial automation passing crafted URLs to delete files outside the clone folder; a config-supplied destination that resolved differently at write time vs read time.
Related errors
- Access denied: submodule path escapes the selected worktree
- Path "${filePath}" resolves outside the worktree
- Invalid repository name derived from URL
- Access denied: invalid submodule path
- Access denied: invalid git file path
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/aa7400efb4aa8a30.
Report an issue: GitHub.