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

  1. Use a real, non-symlinked directory as the clone destination; symlinked destinations can resolve outside themselves.
  2. Do not attempt to 'fix' the URL to dodge this guard — it is the last line preventing filesystem escape; fix the destination instead.
  3. On Windows, pass native Windows paths and avoid MSYS/Git Bash path rewriting by using forward-slash absolute paths from the main process.
  4. 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

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


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/aa7400efb4aa8a30. Report an issue: GitHub.