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
- Use an anchor path that resolves inside the project root (relative paths like ./eval/anchor.json).
- If you need a shared anchor, copy or symlink-within-root it into the repo first (the symlink check is separate).
- Validate the path with path.relative(root, resolved) and reject if it starts with '..'.
- 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
- Use anchor paths relative to the project root (e.g. ./eval/anchor.json).
- Never accept absolute or '../' anchor paths from untrusted input.
- Validate manifest `path` fields for containment before loading.
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
- flywheel anchor symlink escapes project root
- basePath contains disallowed characters
- Key contains disallowed characters
- Namespace contains disallowed characters
- invalid receipt ID
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/6db3c96012ca308b.
Report an issue: GitHub.