affaan-m/ECC · error

Nasiko install directory must be a real directory, not a…

Error message

Nasiko install directory must be a real directory, not a symlink.

What it means

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.

Solutions

  1. Replace the symlink with a real directory: `rm <dir> && mkdir <dir>` and move contents back.
  2. Point installDir at the symlink's real target (use realpath) and pass that path instead.
  3. If using dotfile managers, configure them to manage files inside the directory rather than symlinking the directory itself.

Example fix

// before (dotfiles)
ln -s ~/dotfiles/local-bin ~/.local/bin
installNasiko({ yes: true })
// after
rm ~/.local/bin
mkdir ~/.local/bin
# copy contents back, then:
installNasiko({ yes: true })
Defensive patterns

Strategy: validation

Validate before calling

const stats = fs.lstatSync(dir)
if (stats.isSymbolicLink() || !stats.isDirectory()) throw new Error(`installDir must be a real directory: ${dir}`)

Type guard

const isRealDirectory = (dir) => {
  try { const s = fs.lstatSync(dir); return s.isDirectory() && !s.isSymbolicLink() } catch { return false }
}

Try / catch

try {
  await installNasiko({ installDir: dir, yes: true })
} catch (e) {
  if (e.message.includes('not a symlink')) {
    const real = fs.realpathSync(dir)
    return installNasiko({ installDir: real, yes: true })
  }
  throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/84dce8798400e3fb. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/nasiko-release.js:186

function validateInstallDirectory(directory) {
  if (typeof directory !== 'string' || directory.includes('\0') || !path.isAbsolute(directory)) throw new Error('Nasiko install directory must be an absolute path.');
  if (/^(?:\\\\|\\\\\?\\|\\\\\.\\)/.test(directory)) throw new Error('Nasiko install directory must be on a local filesystem.');
  const resolved = path.resolve(directory);
  if (resolved === path.parse(resolved).root) throw new Error('Nasiko cannot install directly into a filesystem root.');
  let ancestor = resolved;
  while (!fs.existsSync(ancestor)) {
    const parent = path.dirname(ancestor);
    if (parent === ancestor) throw new Error('Nasiko install directory has no resolvable filesystem ancestor.');
    ancestor = parent;
  }
  const canonical = fs.realpathSync(ancestor);
  return path.join(canonical, path.relative(ancestor, resolved));
}

function assertPrivateInstallDirectory(directory) {
  const stats = fs.lstatSync(directory);
  if (!stats.isDirectory() || stats.isSymbolicLink()) throw new Error('Nasiko install directory must be a real directory, not a symlink.');
  if (process.platform !== 'win32') {
    if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) throw new Error('Nasiko install directory must be owned by the current user.');
    if ((stats.mode & 0o022) !== 0) throw new Error('Nasiko install directory must not be group- or world-writable.');
  }
}

function metadataPathFor(executable) { return path.join(path.dirname(executable), METADATA_FILENAME); }

function readBoundedRegularFile(filePath, maximumBytes) {
  const noFollow = fs.constants.O_NOFOLLOW;
  const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | (noFollow || 0));
  try {
    const stats = fs.fstatSync(descriptor);
    if (!stats.isFile() || stats.size <= 0 || stats.size > maximumBytes) return null;
    if (!noFollow) {
      const pathStats = fs.lstatSync(filePath);
      if (pathStats.isSymbolicLink()
        || pathStats.dev !== stats.dev

View on GitHub (pinned to 8321021c54)