ruvnet/ruflo · error · Error
unsafe repository-relative path: ${path}
Error message
unsafe repository-relative path: ${path} What it means
normalizeRelativePath() validates that a repository-relative path is safe before it is used by the source-state harness: it must be non-empty, not absolute, not dash-prefixed, and must contain no empty, '.', or '..' path segments. It exists to prevent path traversal and flag-lookalike arguments from entering git invocations and digest computations.
Source
Thrown at v3/@claude-flow/codex/src/harness/repository-state.ts:207
}
assertUnicodeScalarString(path);
if (path !== path.normalize('NFC')) {
throw new Error(`Git path is not NFC-normalized: ${path}`);
}
return normalizeRelativePath(path);
}
function normalizeRelativePath(path: string): string {
assertUnicodeScalarString(path);
if (path.includes('\\')) throw new Error(`ambiguous repository path separator: ${path}`);
const normalized = path.normalize('NFC');
if (
normalized.length === 0
|| isAbsolute(normalized)
|| normalized.startsWith('-')
|| normalized.split('/').some((part) => part === '' || part === '.' || part === '..')
) {
throw new Error(`unsafe repository-relative path: ${path}`);
}
return normalized;
}
function assertNoPathCollisions(paths: readonly string[]): void {
const exact = new Set<string>();
const folded = new Map<string, string>();
for (const path of paths) {
if (exact.has(path)) throw new Error(`duplicate repository path: ${path}`);
exact.add(path);
const key = portableCaseFold(path);
const prior = folded.get(key);
if (prior !== undefined && prior !== path) {
throw new Error(`case-fold repository path collision: ${prior} and ${path}`);
}
folded.set(key, path);
}
}View on GitHub (pinned to fa13ee4ad6)
Solutions
- Pass paths relative to the repository root, produced by path.relative(repoRoot, absolutePath)
- Strip leading './' and reject absolute paths or '..' segments in your own input validation before calling the harness
- If the string came from user input, normalize it (NFC) and validate segments with the same rules first
Example fix
// before
captureEntry(path.join(repoRoot, userInput)); // '/repo/../etc/passwd' -> throws
// after
const rel = path.relative(repoRoot, path.resolve(repoRoot, userInput));
if (rel.startsWith('..') || path.isAbsolute(rel)) throw new TypeError('path outside repo');
captureEntry(rel); Defensive patterns
Strategy: validation
Validate before calling
function isSafeRelativePath(p: string): boolean {
return typeof p === 'string'
&& !p.includes('\\')
&& p.length > 0
&& !path.isAbsolute(p)
&& !p.startsWith('-')
&& !p.split('/').some(seg => seg === '' || seg === '.' || seg === '..');
} Type guard
function isSafeRelativePath(p: unknown): p is string {
return typeof p === 'string'
&& p.length > 0 && !p.includes('\\')
&& !path.isAbsolute(p) && !p.startsWith('-')
&& !p.split('/').some(s => s === '' || s === '.' || s === '..');
} Try / catch
try { harness.capture(rel); } catch (e) { if (e instanceof Error && e.message.startsWith('unsafe repository-relative path')) throw new TypeError(`Invalid input path: ${JSON.stringify(userInput)}`); throw e; } Prevention
- Always derive relative paths via path.relative(repoRoot, absolute)
- Treat user-supplied filenames as untrusted: validate segments before they reach any harness API
- Never path.join an absolute path into a field documented as repository-relative
When it happens
Trigger: Calling the harness with a path like '', '/etc/passwd', '-rf', '../outside/file', 'a//b', 'a/./b', or 'a/../b'. The check also fires on Windows backslash separators (a sibling check throws for those first) and rejects paths that are still absolute after NFC normalization.
Common situations: User- or agent-supplied filenames from CLI args or manifests contain absolute paths, leading './', or '..' segments; symlink targets are fed in unnormalized; paths built with path.join(repoRoot, file) are passed instead of the relative portion.
Related errors
- repository path escapes root: ${candidate}
- 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/8e3ee80241a183c1.
Report an issue: GitHub.