ruvnet/ruflo · error

Path traversal blocked: ${resolved}

Error message

Path traversal blocked: ${resolved}

What it means

The second branch of safePathAsync(): when the target file does not exist yet, realpath() cannot resolve it, so the function validates the parent directory instead — resolving the parent with realpath and throwing this traversal error when the parent's real location is outside projectRoot. It exists so not-yet-created files (typical for write targets) get the same containment guarantee as existing ones.

Source

Thrown at v3/@claude-flow/hooks/src/workers/index.ts:57

  const resolved = path.resolve(projectRoot, ...segments);

  try {
    // Resolve symlinks to prevent TOCTOU attacks
    const realResolved = await fs.realpath(resolved).catch(() => resolved);
    const realRoot = await fs.realpath(projectRoot).catch(() => projectRoot);

    if (!realResolved.startsWith(realRoot + path.sep) && realResolved !== realRoot) {
      throw new Error(`Path traversal blocked: ${realResolved}`);
    }
    return realResolved;
  } catch (error) {
    // If file doesn't exist yet, validate the parent directory
    const parent = path.dirname(resolved);
    const realParent = await fs.realpath(parent).catch(() => parent);
    const realRoot = await fs.realpath(projectRoot).catch(() => projectRoot);

    if (!realParent.startsWith(realRoot + path.sep) && realParent !== realRoot) {
      throw new Error(`Path traversal blocked: ${resolved}`);
    }
    return resolved;
  }
}

/**
 * Synchronous path validation (for non-async contexts)
 */
function safePath(projectRoot: string, ...segments: string[]): string {
  const resolved = path.resolve(projectRoot, ...segments);
  const realRoot = path.resolve(projectRoot);

  if (!resolved.startsWith(realRoot + path.sep) && resolved !== realRoot) {
    throw new Error(`Path traversal blocked: ${resolved}`);
  }
  return resolved;
}

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Express output paths strictly relative to projectRoot without '..' segments (e.g. 'output/cfg.json', not '../shared/cfg.json')
  2. Ensure parent directories exist inside the root and are not symlinks escaping it; replace symlinked output dirs with real directories inside the workspace
  3. Pre-validate candidate paths with your own containment check before invoking worker write APIs

Example fix

// before
await worker.write('../shared/cfg.json', data); // parent outside root -> blocked

// after
await worker.write('shared/cfg.json', data); // stays inside projectRoot
Defensive patterns

Strategy: validation

Validate before calling

// For write targets: validate the parent dir stays inside the root
import * as path from 'node:path';
function isSafeWritePath(root: string, relPath: string): boolean {
  const parent = path.dirname(path.resolve(root, relPath));
  const rel = path.relative(path.resolve(root), parent);
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}

Type guard

function isRelativeInside(relPath: string): boolean {
  return !path.isAbsolute(relPath) && relPath.split(path.sep).every((s) => s !== '..');
}

Try / catch

try {
  await worker.write(relPath, data);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Path traversal blocked')) {
    // the parent dir resolved outside projectRoot — reject, do not sanitize by retry
    throw new BadRequestError('write path must stay inside the workspace');
  }
  throw e;
}

Prevention

When it happens

Trigger: Requesting a write path whose parent directory resolves outside the root: segments with ../, an absolute parent elsewhere, or a parent directory that is a symlink pointing outside the workspace — and the final file itself does not exist yet, triggering the parent-check branch.

Common situations: Output directories configured with relative ../ paths that resolve differently depending on cwd; symlinked build/output folders (e.g. symlink to a shared artifacts volume); generated-file paths built from templates concatenated with user input; projectRoot mismatch between environments (CI checkout vs local).

Related errors


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