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 is

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Pass at least one absolute, real directory: new PathValidator({ allowedPrefixes: [process.cwd()] }) or [os.tmpdir()].
  2. Fail fast at config load: if the env/config source yields zero prefixes, abort startup with a clear message instead of constructing the validator.
  3. 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).
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/576086c1ba0ed0fe. Report an issue: GitHub.