{"record":{"id":"84dce8798400e3fb","repo":"affaan-m/ECC","slug":"nasiko-install-directory-must-be-a-real-directory","errorCode":null,"errorMessage":"Nasiko install directory must be a real directory, not a symlink.","messagePattern":"Nasiko install directory must be a real directory, not a symlink\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/nasiko-release.js","lineNumber":186,"sourceCode":"\nfunction validateInstallDirectory(directory) {\n  if (typeof directory !== 'string' || directory.includes('\\0') || !path.isAbsolute(directory)) throw new Error('Nasiko install directory must be an absolute path.');\n  if (/^(?:\\\\\\\\|\\\\\\\\\\?\\\\|\\\\\\\\\\.\\\\)/.test(directory)) throw new Error('Nasiko install directory must be on a local filesystem.');\n  const resolved = path.resolve(directory);\n  if (resolved === path.parse(resolved).root) throw new Error('Nasiko cannot install directly into a filesystem root.');\n  let ancestor = resolved;\n  while (!fs.existsSync(ancestor)) {\n    const parent = path.dirname(ancestor);\n    if (parent === ancestor) throw new Error('Nasiko install directory has no resolvable filesystem ancestor.');\n    ancestor = parent;\n  }\n  const canonical = fs.realpathSync(ancestor);\n  return path.join(canonical, path.relative(ancestor, resolved));\n}\n\nfunction assertPrivateInstallDirectory(directory) {\n  const stats = fs.lstatSync(directory);\n  if (!stats.isDirectory() || stats.isSymbolicLink()) throw new Error('Nasiko install directory must be a real directory, not a symlink.');\n  if (process.platform !== 'win32') {\n    if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) throw new Error('Nasiko install directory must be owned by the current user.');\n    if ((stats.mode & 0o022) !== 0) throw new Error('Nasiko install directory must not be group- or world-writable.');\n  }\n}\n\nfunction metadataPathFor(executable) { return path.join(path.dirname(executable), METADATA_FILENAME); }\n\nfunction readBoundedRegularFile(filePath, maximumBytes) {\n  const noFollow = fs.constants.O_NOFOLLOW;\n  const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | (noFollow || 0));\n  try {\n    const stats = fs.fstatSync(descriptor);\n    if (!stats.isFile() || stats.size <= 0 || stats.size > maximumBytes) return null;\n    if (!noFollow) {\n      const pathStats = fs.lstatSync(filePath);\n      if (pathStats.isSymbolicLink()\n        || pathStats.dev !== stats.dev","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/nasiko-release.js#L168-L204","documentation":"assertPrivateInstallDirectory uses lstatSync to inspect the install directory without following symlinks. If the path is not a plain directory, or is a symlink (even to a valid directory), it throws this error. The library refuses symlinked install locations because a symlink can be swapped to redirect writes to an attacker-controlled target.","triggerScenarios":"installNasiko or uninstallNasiko with options.installDir (or the default ~/.local/bin) being a symlink — e.g. a dotfiles-managed symlink, or a directory replaced by a symlink after validation.","commonSituations":"Developers symlink ~/.local/bin into a dotfiles repo (GNU stow, chezmoi); macOS Homebrew setups that symlink bin directories; the install dir being a symlink on Windows junctions; race where another process replaced the directory with a symlink between mkdir and the lstat check.","solutions":["Replace the symlink with a real directory: `rm <dir> && mkdir <dir>` and move contents back.","Point installDir at the symlink's real target (use realpath) and pass that path instead.","If using dotfile managers, configure them to manage files inside the directory rather than symlinking the directory itself."],"exampleFix":"// before (dotfiles)\nln -s ~/dotfiles/local-bin ~/.local/bin\ninstallNasiko({ yes: true })\n// after\nrm ~/.local/bin\nmkdir ~/.local/bin\n# copy contents back, then:\ninstallNasiko({ yes: true })","handlingStrategy":"validation","validationCode":"const stats = fs.lstatSync(dir)\nif (stats.isSymbolicLink() || !stats.isDirectory()) throw new Error(`installDir must be a real directory: ${dir}`)","typeGuard":"const isRealDirectory = (dir) => {\n  try { const s = fs.lstatSync(dir); return s.isDirectory() && !s.isSymbolicLink() } catch { return false }\n}","tryCatchPattern":"try {\n  await installNasiko({ installDir: dir, yes: true })\n} catch (e) {\n  if (e.message.includes('not a symlink')) {\n    const real = fs.realpathSync(dir)\n    return installNasiko({ installDir: real, yes: true })\n  }\n  throw e\n}","preventionTips":["Never symlink the install directory; symlink individual files outside managed paths if needed.","Configure dotfile managers (stow/chezmoi) to manage files inside the directory, not the directory itself.","Run lstat checks on installDir in pre-install scripts.","Re-check the path after any tool that reorganizes your bin directories."],"tags":["symlink","security","install-directory"],"backgroundTag":"incompatible-source-type","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}