ruvnet/ruflo · error · Error

repository path escapes root: ${candidate}

Error message

repository path escapes root: ${candidate}

What it means

assertInsideRepository() computes path.relative(repoRoot, candidate) and throws when the result is '..', starts with '../', or is absolute — i.e. the candidate resolves outside the worktree. It is the final containment check applied to absolute paths built with resolve(repoRoot, path) for untracked files and submodules.

Source

Thrown at v3/@claude-flow/codex/src/harness/repository-state.ts:240

    }
    folded.set(key, path);
  }
}

function repositoryPaths(repoRoot: string, includeUntracked: boolean): readonly string[] {
  const tracked = splitNul(gitBuffer(repoRoot, ['ls-files', '-z'])).map(decodeGitPath);
  const untracked = includeUntracked
    ? splitNul(gitBuffer(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z'])).map(decodeGitPath)
    : [];
  const paths = [...tracked, ...untracked].sort(codeUnitCompare);
  assertNoPathCollisions(paths);
  return paths;
}

function assertInsideRepository(repoRoot: string, candidate: string): void {
  const rel = relative(repoRoot, candidate);
  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
    throw new Error(`repository path escapes root: ${candidate}`);
  }
}

function contentDigest(content: Buffer): ContentDigest {
  return { algorithm: 'sha256', digest: digest(content), bytes: content.byteLength };
}

function untrackedManifest(repoRoot: string): UntrackedManifest {
  const output = gitBuffer(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']);
  const paths = splitNul(output).map(decodeGitPath).sort(codeUnitCompare);
  assertNoPathCollisions(paths);
  const entries = paths.map((path): UntrackedFileIdentity => {
    const absolute = resolve(repoRoot, path);
    assertInsideRepository(repoRoot, absolute);
    const before = lstatSync(absolute);
    let kind: UntrackedFileIdentity['kind'];
    let content: Buffer;
    if (before.isSymbolicLink()) {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Re-derive repoRoot with `git rev-parse --show-toplevel` and realpath before capture
  2. Inspect suspicious entries: `git ls-files | grep -E '(^|/)\.\./|^/'`
  3. If the index is corrupted, rebuild it (`rm .git/index && git reset`) or re-clone
Defensive patterns

Strategy: validation

Validate before calling

const rel = path.relative(repoRoot, path.resolve(repoRoot, entry));
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) throw new Error('entry escapes repo');

Prevention

When it happens

Trigger: A path from `git ls-files` that, after resolve(), lands outside repoRoot: an index entry containing '..' segments or an absolute path, or a repoRoot computed from a different (moved) directory than the one the index belongs to.

Common situations: The repository directory was moved/renamed while a long-lived process still holds the old absolute repoRoot; a hand-crafted or corrupted index contains traversal entries; repoRoot was passed as a symlink that later diverges from the real location.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/2cb07c5ad24a0f43. Report an issue: GitHub.