{"record":{"id":"a6195d64f40ac106","repo":"jackwener/OpenCLI","slug":"output-path-is-not-a-safe-directory-resolved","errorCode":null,"errorMessage":"output path is not a safe directory: ${resolved}","messagePattern":"output path is not a safe directory: (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/novel-download-utils.js","lineNumber":79,"sourceCode":"  }\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  }\n  if (!fs.statSync(canonicalAncestor).isDirectory()) {\n    throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);\n  }\n  return path.join(canonicalAncestor, ...missingParts);","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/novel-download-utils.js#L61-L97","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the filesystem is healthy and the mount point exists (mount, df).","Re-run the command; a transient race may resolve itself.","Pick an output directory whose parent (e.g. cwd or home) definitely exists.","Ensure no concurrent cleanup job deletes the target directory tree during the run."],"exampleFix":"// before\n-o /mnt/ghost-drive/novels   // broken FUSE mount\n// after\n-o ~/novels","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\ntry {\n  fs.statSync(path.resolve(output).split(path.sep)[0] || '/');\n} catch (e) {\n  throw new Error(`Filesystem root unusable: ${e.code}`);\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('Could not anchor output path to an existing directory; check mounts and retry.');\n  } else throw e;\n}","preventionTips":["Verify mounts are healthy (df, mount) before large batch downloads.","Avoid running downloads from paths on broken/virtual filesystems.","Do not concurrently delete the output tree while a download runs.","Re-run once if a transient race is suspected."],"tags":["filesystem","path","race-condition"],"backgroundTag":"path-not-accessible","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}