jackwener/OpenCLI · error · ArgumentError
output path must not be a symbolic link: ${resolved}
Error message
output path must not be a symbolic link: ${resolved} What it means
If the resolved output path itself exists but is a symbolic link, normalizePixivOutputRoot rejects it with ArgumentError. The library refuses symlinked output roots to prevent writes escaping the intended location (symlink-based path traversal / overwriting files elsewhere).
Source
Thrown at clis/pixiv/novel-download-utils.js:86
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);
}
export function pixivPathEntryExists(target) {
try {
fs.lstatSync(target);
return true;
} catch (error) {View on GitHub (pinned to 49907e53dc)
Solutions
- Point --output at the real directory (the symlink's target) instead of the symlink itself.
- Remove the symlink and replace it with a real directory (mkdir) if the direct path is required.
- Use a bind mount or hardlink the contents instead of symlinking the directory.
- If you intentionally need symlink support, copy files through the resolved target path yourself.
Example fix
// before (shell) $ ln -s /mnt/bigdrive/pixiv ~/pixiv $ pixiv-novel download 12345 -o ~/pixiv # refused // after $ pixiv-novel download 12345 -o /mnt/bigdrive/pixiv
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
const st = fs.lstatSync(path.resolve(output)); // throws if missing; check before calling
if (st.isSymbolicLink()) {
output = fs.realpathSync(output); // use the real target instead
} Type guard
function isRealDirectory(p) {
try { return fs.lstatSync(p).isDirectory(); } catch { return false; }
} Try / catch
try {
await downloadNovel(id, { output });
} catch (e) {
if (e.message.includes('must not be a symbolic link')) {
output = fs.realpathSync(output);
await downloadNovel(id, { output });
} else throw e;
} Prevention
- Pass the symlink's target (realpath) as --output, not the link itself.
- Check with fs.lstatSync(path).isSymbolicLink() before configuring output.
- Prefer bind mounts or config that stores the real directory path.
- Avoid 'current/latest' symlink patterns for download output roots.
When it happens
Trigger: Calling normalizePixivOutputRoot (via outputRoot/outputDir/output) where the resolved output path is a symlink — e.g. ~/pixiv is a symlink to another disk, or 'latest' symlink pointing to a snapshot directory.
Common situations: Users who symlink their downloads folder (common with Dropbox/OneDrive setups or 'current -> releases' patterns) hit this guard; security tooling also intentionally creates such links to test traversal defenses.
Related errors
- output path is not a safe directory: ${ancestor}
- Plugin path "${subPath}" escapes repo root.
- Expected monorepo plugin link at ${linkPath} to be a symlink
- Invalid skill path: ${relativePath}
- Invalid skill path: ${raw}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/3ced199eac627e7d.
Report an issue: GitHub.