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
- Express output paths strictly relative to projectRoot without '..' segments (e.g. 'output/cfg.json', not '../shared/cfg.json')
- Ensure parent directories exist inside the root and are not symlinks escaping it; replace symlinked output dirs with real directories inside the workspace
- 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
- Express generated-file paths as root-relative without '..' segments
- Create output directories inside the workspace instead of symlinking to external volumes
- Keep cwd-independent projectRoot so '../' in configs resolves consistently
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
- Path traversal blocked: ${realResolved}
- localSingleEntryPageRank: sourceIndex ${src} out of range [0
- AI job registry is a symlink (refusing): ${path}
- AI budget file is a symlink (refusing): ${path}
- Repo-supervisor file is a symlink (refusing): ${path}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/27c34ae31fb08fd3.
Report an issue: GitHub.