ruvnet/ruflo · error · PathValidatorError
EMPTY_PREFIXES
EMPTY_PREFIXES
Error message
At least one allowed prefix must be specified
What it means
PathValidator's constructor is the one config surface with no default: allowedPrefixes must be a non-empty array. Everything else (blockedExtensions, blockedNames, maxPathLength, resolveSymlinks, allowNonExistent, allowHidden) falls back to sane defaults, but an empty prefix list throws PathValidatorError EMPTY_PREFIXS because a validator that permits nothing is almost certainly a config bug.
Source
Thrown at v3/@claude-flow/security/src/path-validator.ts:264
* every call would hand that control to whoever can write the link, and cost
* a syscall per validation. Callers needing to follow a moved target should
* construct a new validator.
*/
private readonly canonicalPrefixes: string[];
constructor(config: PathValidatorConfig) {
this.config = {
allowedPrefixes: config.allowedPrefixes,
blockedExtensions: config.blockedExtensions ?? DEFAULT_BLOCKED_EXTENSIONS,
blockedNames: config.blockedNames ?? DEFAULT_BLOCKED_NAMES,
maxPathLength: config.maxPathLength ?? 4096,
resolveSymlinks: config.resolveSymlinks ?? true,
allowNonExistent: config.allowNonExistent ?? true,
allowHidden: config.allowHidden ?? false,
};
if (this.config.allowedPrefixes.length === 0) {
throw new PathValidatorError(
'At least one allowed prefix must be specified',
'EMPTY_PREFIXES'
);
}
// Pre-resolve all prefixes, lexically and canonically. #3010 — validate()
// canonicalizes the *candidate* through fs.realpath (when resolveSymlinks
// is on, the default) but prefixes were only ever path.resolve()d, never
// realpath'd. On any platform where the prefix itself is reached through a
// symlink (e.g. macOS os.tmpdir() -> /var/folders/... while /var is itself
// a symlink to /private/var), the realpath'd candidate can never match the
// non-realpath'd prefix, and every path under that prefix is rejected as
// "outside allowed directories" — including the prefix's own contents.
this.resolvedPrefixes = this.config.allowedPrefixes.map(p =>
path.resolve(p)
);
// Always a distinct array — `addPrefix` appends to both, so aliasing the
// lexical list would push twice. With resolveSymlinks off no candidate isView on GitHub (pinned to fa13ee4ad6)
Solutions
- Pass at least one absolute, real directory: new PathValidator({ allowedPrefixes: [process.cwd()] }) or [os.tmpdir()].
- Fail fast at config load: if the env/config source yields zero prefixes, abort startup with a clear message instead of constructing the validator.
- Remember prefixes are canonicalized with realpath at construction (see #3010 note), so pass the symlink-resolved path on platforms like macOS (/private/var not /var).
- If 'deny everything' is truly intended, skip creating a PathValidator rather than passing an empty array.
Example fix
// before
const dirs = process.env.ALLOWED_DIRS?.split(',').filter(Boolean) ?? [];
const validator = new PathValidator({ allowedPrefixes: dirs }); // throws EMPTY_PREFIXS when unset
// after
const dirs = process.env.ALLOWED_DIRS?.split(',').map((d) => d.trim()).filter(Boolean);
if (!dirs?.length) throw new Error('ALLOWED_DIRS must list at least one directory');
const validator = new PathValidator({ allowedPrefixes: await Promise.all(dirs.map((d) => fs.realpath(d))) }); Defensive patterns
Strategy: validation
Validate before calling
if (!Array.isArray(prefixes) || prefixes.length === 0) {
throw new Error(
`PathValidator requires >= 1 allowed prefix, got: ${JSON.stringify(prefixes)}`
);
}
const validator = new PathValidator({ allowedPrefixes: prefixes }); Type guard
function hasAllowedPrefixes(cfg: unknown): cfg is { allowedPrefixes: string[] } {
return (
!!cfg && typeof cfg === 'object' &&
Array.isArray((cfg as any).allowedPrefixes) &&
(cfg as any).allowedPrefixes.length > 0 &&
(cfg as any).allowedPrefixes.every((p: unknown) => typeof p === 'string' && path.isAbsolute(p))
);
} Try / catch
try {
return new PathValidator(config);
} catch (err) {
if (err instanceof PathValidatorError && err.code === 'EMPTY_PREFIXES') {
failStartup('allowedPrefixes is empty — set WORKSPACE_DIRS');
}
throw err;
} Prevention
- Validate the prefix list at config load and abort startup loudly instead of constructing a broken validator.
- Build prefixes with filter(Boolean) after splitting env vars so empty strings never reach the constructor.
- Remember the constructor realpath's prefixes; pass resolved directories to avoid later VALIDATION_FAILED surprises.
When it happens
Trigger: new PathValidator({ allowedPrefixes: [] }); building prefixes from an env var that is unset (config.allowedPrefixes = (process.env.ALLOWED_DIRS || '').split(':') yields [''] or [''] after filtering); filtering a configured list down to zero entries before construction.
Common situations: Per-tenant workspace roots resolved from a database column that is NULL; deployment where the sandbox dirs env var was never set; refactoring that moved prefix computation after construction order changed.
Related errors
- VALIDATION_FAILED
- Validation failed: ${result.error}
- ${toolsJson} must contain a JSON array of {name, description
- basePath contains disallowed characters
- memory path contains disallowed characters
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/576086c1ba0ed0fe.
Report an issue: GitHub.