ruvnet/ruflo · error · Error

flywheel anchor path must stay inside project root

Error message

flywheel anchor path must stay inside project root

What it means

Thrown by containedPath() in harness-project-anchor.ts during the LEXICAL path-containment check (before resolving symlinks). The requested anchor path, when resolved relative to the project root, produces a relative path starting with '..' or an absolute path — i.e. it escapes the project root purely by its string form. This is the first layer of path-traversal defense for project-local flywheel anchors.

Source

Thrown at v3/@claude-flow/cli/src/services/harness-project-anchor.ts:64

}

export interface LoadFlywheelAnchorOptions {
  anchorPath?: string;
  anchorHash?: string;
  manifestPath?: string;
}

function normalizeHash(value: string): string {
  const trimmed = value.trim().toLowerCase();
  return trimmed.startsWith('sha256:') ? trimmed : `sha256:${trimmed}`;
}

function containedPath(projectRoot: string, requested: string): string {
  const root = realpathSync(resolve(projectRoot));
  const absolute = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
  const lexical = relative(root, absolute);
  if (lexical === '..' || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) {
    throw new Error('flywheel anchor path must stay inside project root');
  }
  const actual = realpathSync(absolute);
  const physical = relative(root, actual);
  if (physical === '..' || physical.startsWith(`..${sep}`) || isAbsolute(physical)) {
    throw new Error('flywheel anchor symlink escapes project root');
  }
  return actual;
}

function parseTasks(path: string): { version: string; tasks: HumanEvalTask[] } {
  const parsed = JSON.parse(readFileSync(path, 'utf8')) as {
    schemaVersion?: string;
    version?: string;
    tasks?: HumanEvalTask[];
  };
  if (parsed.schemaVersion && parsed.schemaVersion !== PROJECT_ANCHOR_SCHEMA) {
    throw new Error(`unsupported flywheel anchor schema: ${parsed.schemaVersion}`);
  }

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Use an anchor path that resolves inside the project root (relative paths like ./eval/anchor.json).
  2. If you need a shared anchor, copy or symlink-within-root it into the repo first (the symlink check is separate).
  3. Validate the path with path.relative(root, resolved) and reject if it starts with '..'.
  4. For manifest `path` fields, use paths relative to the manifest location that stay inside root.

Example fix

// before
loadEffectiveFlywheelAnchor(root, { anchorPath: '../shared/anchor.json', anchorHash });
// after
cp ../shared/anchor.json ./eval/anchor.json
loadEffectiveFlywheelAnchor(root, { anchorPath: './eval/anchor.json', anchorHash });
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, relative, isAbsolute, sep } from 'node:path';
function assertPathInsideRoot(root: string, requested: string): void {
  const absolute = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
  const rel = relative(root, absolute);
  if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
    throw new Error(`anchor path escapes project root: ${requested}`);
  }
}
assertPathInsideRoot(root, options.anchorPath);

Type guard

const isPathInsideRoot = (root: string, requested: string): boolean => {
  const rel = relative(root, isAbsolute(requested) ? resolve(requested) : resolve(root, requested));
  return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
};

Try / catch

try {
  loadEffectiveFlywheelAnchor(root, { anchorPath, anchorHash });
} catch (e) {
  if (e instanceof Error && /must stay inside project root/.test(e.message)) {
    throw new Error(`anchor path rejected (traversal): ${anchorPath}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling loadEffectiveFlywheelAnchor(root, { anchorPath: '../sibling/anchor.json' }) or { anchorPath: '/etc/anchor.json' } or any path that resolves outside the project root. Also via a manifest whose `path` field points outside.

Common situations: A manifest with an absolute path to a shared anchor outside the repo; a relative '../' path intended to reference a monorepo sibling; user-supplied anchor path that wasn't validated; a path-injection attempt.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/6db3c96012ca308b. Report an issue: GitHub.