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
- Re-derive repoRoot with `git rev-parse --show-toplevel` and realpath before capture
- Inspect suspicious entries: `git ls-files | grep -E '(^|/)\.\./|^/'`
- 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
- Always resolve entries against a freshly derived repoRoot (rev-parse --show-toplevel + realpath)
- Never trust index-sourced or network-sourced paths without containment checks
- Re-derive repoRoot if the process can outlive directory moves
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
- unsafe repository-relative path: ${path}
- unsafe build input path: ${value}
- unknown game "${key}". Known: ${Object.keys(GAMES).join(', '
- ${label} contains null bytes
- ${label} contains shell metacharacters
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/2cb07c5ad24a0f43.
Report an issue: GitHub.