ruvnet/ruflo · critical · Error
registry path escapes owned worktree root: ${actual}
Error message
registry path escapes owned worktree root: ${actual} What it means
Every assignment path in a registry record must resolve to the run's own worktree root (<sibling-of-repoRoot>/.ruflo-worktrees/<repoName>/<runId>) or somewhere beneath it. A path resolving outside that prefix means the registry was tampered with or was created under a different layout, and downstream operations (git worktree remove, git merge of assignment branches) would act on directories the run does not own — so status() refuses the entire record.
Source
Thrown at v3/@claude-flow/codex/src/worktrees/coordinator.ts:110
try { git(this.repoRoot, ['worktree', 'remove', assignment.path]); } catch { /* retain work for recovery */ }
}
throw error;
}
}
status(runId: string): WorktreeRunRecord {
assertId(runId, 'run id');
const file = this.registryPath(runId);
if (!existsSync(file)) throw new Error(`unknown worktree run: ${runId}`);
const record = JSON.parse(readFileSync(file, 'utf8')) as WorktreeRunRecord;
if (record.version !== 1 || record.runId !== runId || resolve(record.repoRoot) !== this.repoRoot) {
throw new Error('invalid worktree registry record');
}
const expectedPrefix = resolve(join(this.worktreeBase, runId));
for (const assignment of record.assignments) {
const actual = resolve(assignment.path);
if (actual !== expectedPrefix && !actual.startsWith(`${expectedPrefix}/`)) {
throw new Error(`registry path escapes owned worktree root: ${actual}`);
}
}
return record;
}
integrate(runId: string, agentIds?: string[]): { merged: string[] } {
const record = this.status(runId);
const wanted = agentIds ? new Set(agentIds) : null;
const merged: string[] = [];
for (const assignment of record.assignments) {
if (assignment.readOnly || (wanted && !wanted.has(assignment.agentId))) continue;
git(this.repoRoot, ['merge', '--no-ff', '--no-edit', assignment.branch]);
merged.push(assignment.agentId);
}
return { merged };
}
cleanup(runId: string): { removed: string[]; retained: string[] } {View on GitHub (pinned to fa13ee4ad6)
Solutions
- Keep the repository at the same absolute path for the lifetime of a run; finish or clean up runs before moving/renaming the repo or its parent directory
- If the registry is stale from a relocation, remove the run's registry file and its leftover worktrees, then re-prepare with a fresh run id
- Never modify assignment.path values — they are owned by the coordinator and validated against the worktree base
Example fix
// before
// repo moved from /old/workspace/proj to /new/workspace/proj between runs
coordinator.status('run-1'); // throws: registry path escapes owned worktree root
// after
// clean the orphaned run and re-prepare at the new location
fs.rmSync('/old/workspace/.ruflo-worktrees/proj/run-1', { recursive: true, force: true });
coordinator.prepare('run-1-fresh', agents); Defensive patterns
Strategy: validation
Validate before calling
import { resolve, relative, isAbsolute } from 'node:path';
function isUnderRoot(root: string, candidate: string): boolean {
const rel = relative(resolve(root), resolve(candidate));
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
}
// Before integrate(): verify every recorded path stays under the run's worktree root
for (const a of record.assignments) {
if (!isUnderRoot(join(worktreeBase, runId), a.path)) throw new Error('registry tampered');
} Try / catch
try {
coordinator.integrate(runId);
} catch (e) {
if (e instanceof Error && e.message.includes('escapes owned worktree root')) {
// quarantine the run: do NOT edit paths to 'fix' them; clean and re-prepare
} else throw e;
} Prevention
- Keep the repo at a stable absolute path for the lifetime of a run; clean runs before relocating
- Treat registry assignment paths as opaque coordinator-owned data — never rewrite them
When it happens
Trigger: Editing assignment.path in <runId>.json to a relative escape such as ../../elsewhere; moving or renaming the repository (or its parent directory) between prepare() and status()/integrate() so resolve(record paths) no longer sits under the current worktreeBase; symlinks inside the worktree root resolving outside it.
Common situations: Relocating or renaming the repo directory while a run is in progress; restoring registry files from backup into a different checkout; hand-crafted registry records pointing at arbitrary absolute paths.
Related errors
- Invalid git ref: suspicious pattern
- unknown worktree run: ${runId}
- unknown strategy "${name}". Available: ${roster.map((r) => r
- localSingleEntryPageRank: sourceIndex ${src} out of range [0
- ${label} escapes project directory
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/2cccfaccdb645fac.
Report an issue: GitHub.