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

  1. Pass paths relative to the repository root, produced by path.relative(repoRoot, absolutePath)
  2. Strip leading './' and reject absolute paths or '..' segments in your own input validation before calling the harness
  3. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/8e3ee80241a183c1. Report an issue: GitHub.