jackwener/OpenCLI · error · ArgumentError
output path is not a safe directory: ${ancestor}
Error message
output path is not a safe directory: ${ancestor} What it means
While walking up the path to find the deepest existing ancestor, the function calls fs.lstatSync on each ancestor. If lstat fails with anything other than ENOENT (e.g. EACCES, ELOOP, ENOTDIR, EPERM), the path cannot be proven safe, so an ArgumentError naming the failing ancestor is thrown. ENOENT is tolerated because missing segments are created later.
Source
Thrown at clis/pixiv/novel-download-utils.js:75
export function normalizePixivOutputRoot(value, fallback) {
if (value !== undefined && typeof value !== 'string') {
throw new ArgumentError('output must be a directory path');
}
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}`);
}View on GitHub (pinned to 49907e53dc)
Solutions
- Check and fix permissions on the failing ancestor directory (chmod/chown so the running user can traverse it).
- Remove or rename any regular file that occupies a component of the output path.
- Check for symlink loops (ls -l / find -type l) and break the cycle.
- Choose a different output directory inside a writable, traversable location such as your project or home directory.
Example fix
// before (shell) $ pixiv-novel download 12345 -o /root/novels # EACCES // after (shell) $ pixiv-novel download 12345 -o ~/novels
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
function pathIsInspectable(p) {
try { fs.accessSync(p, fs.constants.X_OK); return true; }
catch (e) { console.error(`Cannot traverse ${p}: ${e.code}`); return false; }
} Try / catch
try {
await downloadNovel(id, { output });
} catch (e) {
if (e.name === 'ArgumentError' && e.message.includes('not a safe directory')) {
console.error(`Fix permissions/type on path component: ${e.message}`);
} else throw e;
} Prevention
- Run the tool as a user with execute permission on every ancestor of the output path.
- Never point --output at a path segment inside a regular file.
- Avoid symlink loops in download directories.
- Prefer simple paths under your home or project directory.
When it happens
Trigger: The output path (or an ancestor) is unreadable: a permission-denied directory (EACCES/EPERM), a symlink loop (ELOOP), a non-directory component in the middle of the path (ENOTDIR), or an I/O error on the volume.
Common situations: Running the CLI as a user without execute permission on a parent directory (e.g. /root/out); output path like ./file.txt/subdir where file.txt is a regular file; mounted network volume that went offline.
Related errors
- Failed to inspect Pixiv download target ${target}: ${error?.
- File could not be read: ${path}
- Receipt file cannot be read: ${receipt}
- Could not store Midjourney ${kind} at ${filePath}: ${errorMe
- Could not read Midjourney usage snapshots: ${errorMessage(er
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/acf881e818d94ad2.
Report an issue: GitHub.