ruvnet/ruflo · error · Error
case-fold repository path collision: ${prior} and ${path}
Error message
case-fold repository path collision: ${prior} and ${path} What it means
assertNoPathCollisions() also folds every path case-insensitively (portableCaseFold) and rejects two distinct paths that are equal after folding, e.g. 'README.md' and 'Readme.md'. The harness needs unambiguous path identity because the capture is content-addressed and may be checked out on case-insensitive filesystems where the two names cannot coexist.
Source
Thrown at v3/@claude-flow/codex/src/harness/repository-state.ts:221
|| 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);
}
}
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)) {View on GitHub (pinned to fa13ee4ad6)
Solutions
- Find the pair: `git ls-files | awk '{print tolower($0)}' | sort | uniq -d`
- Rename one side with a two-step `git mv` (git mv -f to a temp name, then to the cased name you want) since one-shot case renames are no-ops on case-insensitive filesystems
- If one file is junk, delete it and commit the rename
Example fix
# two-step case rename on macOS/Windows git mv Readme.md Readme.md.tmp git mv Readme.md.tmp README.md git commit -m "fix case-fold collision"
Defensive patterns
Strategy: validation
Validate before calling
const fold = (s: string) => s.toLowerCase();
const seen = new Set<string>();
for (const p of paths) {
const k = fold(p);
if (seen.has(k)) throw new Error(`case collision: ${p}`);
seen.add(k);
} Prevention
- Add a CI lint that runs `git ls-files | awk '{print tolower($0)}' | sort | uniq -d` and fails on output
- Do case renames in two git-mv steps on case-insensitive platforms
- Review merges that touch files whose names differ only by case
When it happens
Trigger: The worktree contains two files (or a file and a directory name component) differing only by letter case — typically after extracting an archive, merging branches from case-insensitive machines, or copying trees on macOS/Windows.
Common situations: A merge brings in 'Makefile' while the repo has 'makefile'; archives extracted on macOS/Windows create case-variant duplicates; Linux CI then fails here while the developer's machine appears fine.
Related errors
- case-fold collision in declared build inputs
- Git returned a non-NUL-terminated path list
- Git path is not valid round-trip UTF-8
- unsupported untracked repository entry: ${path}
- repository changed while hashing untracked entry: ${path}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/d25fe2e17e9b3fec.
Report an issue: GitHub.