{"record":{"id":"2cb07c5ad24a0f43","repo":"ruvnet/ruflo","slug":"repository-path-escapes-root-candidate","errorCode":null,"errorMessage":"repository path escapes root: ${candidate}","messagePattern":"repository path escapes root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/codex/src/harness/repository-state.ts","lineNumber":240,"sourceCode":"    }\n    folded.set(key, path);\n  }\n}\n\nfunction repositoryPaths(repoRoot: string, includeUntracked: boolean): readonly string[] {\n  const tracked = splitNul(gitBuffer(repoRoot, ['ls-files', '-z'])).map(decodeGitPath);\n  const untracked = includeUntracked\n    ? splitNul(gitBuffer(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z'])).map(decodeGitPath)\n    : [];\n  const paths = [...tracked, ...untracked].sort(codeUnitCompare);\n  assertNoPathCollisions(paths);\n  return paths;\n}\n\nfunction assertInsideRepository(repoRoot: string, candidate: string): void {\n  const rel = relative(repoRoot, candidate);\n  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {\n    throw new Error(`repository path escapes root: ${candidate}`);\n  }\n}\n\nfunction contentDigest(content: Buffer): ContentDigest {\n  return { algorithm: 'sha256', digest: digest(content), bytes: content.byteLength };\n}\n\nfunction untrackedManifest(repoRoot: string): UntrackedManifest {\n  const output = gitBuffer(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']);\n  const paths = splitNul(output).map(decodeGitPath).sort(codeUnitCompare);\n  assertNoPathCollisions(paths);\n  const entries = paths.map((path): UntrackedFileIdentity => {\n    const absolute = resolve(repoRoot, path);\n    assertInsideRepository(repoRoot, absolute);\n    const before = lstatSync(absolute);\n    let kind: UntrackedFileIdentity['kind'];\n    let content: Buffer;\n    if (before.isSymbolicLink()) {","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/codex/src/harness/repository-state.ts#L222-L258","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const rel = path.relative(repoRoot, path.resolve(repoRoot, entry));\nif (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) throw new Error('entry escapes repo');","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["path-validation","security","git","harness"],"backgroundTag":"path-traversal-rejected","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}