{"record":{"id":"2cccfaccdb645fac","repo":"ruvnet/ruflo","slug":"registry-path-escapes-owned-worktree-root-actua","errorCode":null,"errorMessage":"registry path escapes owned worktree root: ${actual}","messagePattern":"registry path escapes owned worktree root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"v3/@claude-flow/codex/src/worktrees/coordinator.ts","lineNumber":110,"sourceCode":"        try { git(this.repoRoot, ['worktree', 'remove', assignment.path]); } catch { /* retain work for recovery */ }\n      }\n      throw error;\n    }\n  }\n\n  status(runId: string): WorktreeRunRecord {\n    assertId(runId, 'run id');\n    const file = this.registryPath(runId);\n    if (!existsSync(file)) throw new Error(`unknown worktree run: ${runId}`);\n    const record = JSON.parse(readFileSync(file, 'utf8')) as WorktreeRunRecord;\n    if (record.version !== 1 || record.runId !== runId || resolve(record.repoRoot) !== this.repoRoot) {\n      throw new Error('invalid worktree registry record');\n    }\n    const expectedPrefix = resolve(join(this.worktreeBase, runId));\n    for (const assignment of record.assignments) {\n      const actual = resolve(assignment.path);\n      if (actual !== expectedPrefix && !actual.startsWith(`${expectedPrefix}/`)) {\n        throw new Error(`registry path escapes owned worktree root: ${actual}`);\n      }\n    }\n    return record;\n  }\n\n  integrate(runId: string, agentIds?: string[]): { merged: string[] } {\n    const record = this.status(runId);\n    const wanted = agentIds ? new Set(agentIds) : null;\n    const merged: string[] = [];\n    for (const assignment of record.assignments) {\n      if (assignment.readOnly || (wanted && !wanted.has(assignment.agentId))) continue;\n      git(this.repoRoot, ['merge', '--no-ff', '--no-edit', assignment.branch]);\n      merged.push(assignment.agentId);\n    }\n    return { merged };\n  }\n\n  cleanup(runId: string): { removed: string[]; retained: string[] } {","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/codex/src/worktrees/coordinator.ts#L92-L128","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n// repo moved from /old/workspace/proj to /new/workspace/proj between runs\ncoordinator.status('run-1'); // throws: registry path escapes owned worktree root\n\n// after\n// clean the orphaned run and re-prepare at the new location\nfs.rmSync('/old/workspace/.ruflo-worktrees/proj/run-1', { recursive: true, force: true });\ncoordinator.prepare('run-1-fresh', agents);","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute } from 'node:path';\nfunction isUnderRoot(root: string, candidate: string): boolean {\n  const rel = relative(resolve(root), resolve(candidate));\n  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));\n}\n// Before integrate(): verify every recorded path stays under the run's worktree root\nfor (const a of record.assignments) {\n  if (!isUnderRoot(join(worktreeBase, runId), a.path)) throw new Error('registry tampered');\n}","typeGuard":null,"tryCatchPattern":"try {\n  coordinator.integrate(runId);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('escapes owned worktree root')) {\n    // quarantine the run: do NOT edit paths to 'fix' them; clean and re-prepare\n  } else throw e;\n}","preventionTips":["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"],"tags":["worktrees","path-traversal","security","registry","git"],"backgroundTag":"path-traversal-blocked","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}