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
- Put the tasks file inside the project root and pass a project-relative path (or an absolute path that resolves within the root)
- Fix the `projectRoot` argument so it names the actual project directory
- 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
- Always pass anchor paths relative to the project root
- Double-check projectRoot when the caller is a daemon or CLI running from another cwd
- Vendor shared task files into `.claude/eval/` instead of reaching outside the repo
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
- flywheel anchor symlink escapes project root
- anchor task has no query
- anchor task requires non-empty string labels
- anchorPath and anchorHash must be supplied together
- basePath contains disallowed characters
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)