jackwener/OpenCLI · error · ArgumentError

output path is not a safe directory: ${resolved}

Error message

output path is not a safe directory: ${resolved}

What it means

If every lstat attempt fails with ENOENT and the walk reaches the filesystem root (path.dirname(ancestor) === ancestor) without finding an existing ancestor, the function throws an ArgumentError naming the fully resolved path. This means the resolved path could not be anchored to any existing directory on the filesystem, so creating it is considered unsafe.

Source

Thrown at clis/pixiv/novel-download-utils.js:79

  }
  const raw = value ?? fallback;
  if (!raw || raw.includes('\0')) {
    throw new ArgumentError('output must be a non-empty directory path');
  }
  const resolved = path.resolve(raw);
  let ancestor = resolved;
  const missingParts = [];
  let ancestorStat;
  while (!ancestorStat) {
    try {
      ancestorStat = fs.lstatSync(ancestor);
    } catch (error) {
      if (error?.code !== 'ENOENT') {
        throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);
      }
      const parent = path.dirname(ancestor);
      if (parent === ancestor) {
        throw new ArgumentError(`output path is not a safe directory: ${resolved}`);
      }
      missingParts.unshift(path.basename(ancestor));
      ancestor = parent;
    }
  }
  if (ancestor === resolved && ancestorStat.isSymbolicLink()) {
    throw new ArgumentError(`output path must not be a symbolic link: ${resolved}`);
  }
  let canonicalAncestor;
  try {
    canonicalAncestor = fs.realpathSync.native(ancestor);
  } catch {
    throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);
  }
  if (!fs.statSync(canonicalAncestor).isDirectory()) {
    throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);
  }
  return path.join(canonicalAncestor, ...missingParts);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Verify the filesystem is healthy and the mount point exists (mount, df).
  2. Re-run the command; a transient race may resolve itself.
  3. Pick an output directory whose parent (e.g. cwd or home) definitely exists.
  4. Ensure no concurrent cleanup job deletes the target directory tree during the run.

Example fix

// before
-o /mnt/ghost-drive/novels   // broken FUSE mount
// after
-o ~/novels
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
try {
  fs.statSync(path.resolve(output).split(path.sep)[0] || '/');
} catch (e) {
  throw new Error(`Filesystem root unusable: ${e.code}`);
}

Try / catch

try {
  await downloadNovel(id, { output });
} catch (e) {
  if (e.name === 'ArgumentError' && e.message.includes('not a safe directory')) {
    console.error('Could not anchor output path to an existing directory; check mounts and retry.');
  } else throw e;
}

Prevention

When it happens

Trigger: The resolved output path's entire chain of ancestors fails lstat with ENOENT up to the root — practically only when lstat on a path component returns ENOENT spuriously (rare races) or on exotic/virtual filesystems where the root itself is not statable.

Common situations: Extremely rare: a race where an existing ancestor is deleted mid-walk, a broken FUSE mount reporting ENOENT for everything, or resolving on an unusable path root.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/a6195d64f40ac106. Report an issue: GitHub.