{"record":{"id":"acf881e818d94ad2","repo":"jackwener/OpenCLI","slug":"output-path-is-not-a-safe-directory-ancestor","errorCode":null,"errorMessage":"output path is not a safe directory: ${ancestor}","messagePattern":"output path is not a safe directory: (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/novel-download-utils.js","lineNumber":75,"sourceCode":"\nexport function normalizePixivOutputRoot(value, fallback) {\n  if (value !== undefined && typeof value !== 'string') {\n    throw new ArgumentError('output must be a directory path');\n  }\n  const raw = value ?? fallback;\n  if (!raw || raw.includes('\\0')) {\n    throw new ArgumentError('output must be a non-empty directory path');\n  }\n  const resolved = path.resolve(raw);\n  let ancestor = resolved;\n  const missingParts = [];\n  let ancestorStat;\n  while (!ancestorStat) {\n    try {\n      ancestorStat = fs.lstatSync(ancestor);\n    } catch (error) {\n      if (error?.code !== 'ENOENT') {\n        throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);\n      }\n      const parent = path.dirname(ancestor);\n      if (parent === ancestor) {\n        throw new ArgumentError(`output path is not a safe directory: ${resolved}`);\n      }\n      missingParts.unshift(path.basename(ancestor));\n      ancestor = parent;\n    }\n  }\n  if (ancestor === resolved && ancestorStat.isSymbolicLink()) {\n    throw new ArgumentError(`output path must not be a symbolic link: ${resolved}`);\n  }\n  let canonicalAncestor;\n  try {\n    canonicalAncestor = fs.realpathSync.native(ancestor);\n  } catch {\n    throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);\n  }","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/novel-download-utils.js#L57-L93","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (shell)\n$ pixiv-novel download 12345 -o /root/novels   # EACCES\n// after (shell)\n$ pixiv-novel download 12345 -o ~/novels","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nfunction pathIsInspectable(p) {\n  try { fs.accessSync(p, fs.constants.X_OK); return true; }\n  catch (e) { console.error(`Cannot traverse ${p}: ${e.code}`); return false; }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await downloadNovel(id, { output });\n} catch (e) {\n  if (e.name === 'ArgumentError' && e.message.includes('not a safe directory')) {\n    console.error(`Fix permissions/type on path component: ${e.message}`);\n  } else throw e;\n}","preventionTips":["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."],"tags":["filesystem","permissions","path","symlink"],"backgroundTag":"path-not-accessible","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}