ruvnet/ruflo · error

flywheel anchor path must stay inside project root

Error message

flywheel anchor path must stay inside project root

What it means

`containedPath` is the containment guard for project-local flywheel anchors (#2840): it resolves the real project root, resolves the requested path against it, and rejects any path whose *lexical* relative form escapes the root (`..` prefix, or an absolute path resolving elsewhere). Its purpose is that only files inside the project may serve as labelled anchor tasks, so foreign or system files cannot be silently evaluated against.

Solutions

  1. Put the tasks file inside the project root and pass a project-relative path (or an absolute path that resolves within the root)
  2. Fix the `projectRoot` argument so it names the actual project directory
  3. If the tasks live elsewhere, copy them into the repo (e.g. `.claude/eval/`) and pin them via a manifest or anchorHash

Example fix

// before: anchor outside the project
loadEffectiveFlywheelAnchor(root, { anchorPath: '../shared/eval-tasks.json', anchorHash: h });

// after: tasks vendored into the project
loadEffectiveFlywheelAnchor(root, { anchorPath: '.claude/eval/tasks.json', anchorHash: h });
Defensive patterns

Strategy: validation

Validate before calling

import { isAbsolute, relative, resolve, realpathSync } from 'node:path';

// Mirrors the lexical half of containedPath().
function isContainedLexically(projectRoot: string, requested: string): boolean {
  const root = realpathSync(resolve(projectRoot));
  const absolute = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
  const rel = relative(root, absolute);
  return rel !== '..' && !rel.startsWith(`..${require('node:path').sep}`) && !isAbsolute(rel);
}

if (!isContainedLexically(root, opts.anchorPath)) {
  throw new Error(`anchor path escapes project root: ${opts.anchorPath}`);
}

Try / catch

try {
  return loadEffectiveFlywheelAnchor(root, opts);
} catch (e) {
  if (e?.message === 'flywheel anchor path must stay inside project root') {
    // resolve or vendor the file inside root, fix projectRoot, then retry
    throw new Error(`Anchor path '${opts.anchorPath}' is outside project '${root}'. Copy the tasks into the repo or fix projectRoot.`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `loadEffectiveFlywheelAnchor(root, { anchorPath: '../shared/tasks.json' })`, passing an absolute path outside the project, or passing a wrong `projectRoot` (e.g. cwd one level up) so a legitimately-inside path resolves outside.

Common situations: Monorepo users pointing at a sibling package's tasks file; CI configs feeding absolute paths; callers computing projectRoot from `process.cwd()` when the project lives elsewhere; Windows drive-letter absolute paths.

Related errors


AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15). Data as JSON: /api/errors/6db3c96012ca308b. Report an issue: GitHub.

Appendix: source

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

}

function containedPath(projectRoot: string, requested: string): string {
  // The project root has two equally valid spellings when its path crosses a
  // symlink — on macOS `/tmp/x` and `/private/tmp/x` name the same directory.
  // Comparing a realpath'd root against a NON-realpath'd candidate (as this
  // did) makes every such project look like an escape, so a project anchored
  // anywhere under a symlink was rejected outright. Compare like with like:
  // the lexical guard accepts either spelling of the root, and the symlink
  // guard below still resolves the target and re-checks it physically.
  const rootLexical = resolve(projectRoot);
  const rootPhysical = realpathSync(rootLexical);
  const absolute = isAbsolute(requested) ? resolve(requested) : resolve(rootLexical, requested);
  const escapes = (base: string): boolean => {
    const rel = relative(base, absolute);
    return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);
  };
  if (escapes(rootLexical) && escapes(rootPhysical)) {
    throw new Error('flywheel anchor path must stay inside project root');
  }
  const actual = realpathSync(absolute);
  const physical = relative(rootPhysical, 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 2602b642d9)