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
- 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.
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
- 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.
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
- gate.variant_invalid
- Invalid ECC repo root: missing package.json at
- Nasiko executable must be a regular file, not a symlink.
- Nasiko install directory must not be group- or…
- output artifact must be a regular file
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.devView on GitHub (pinned to 8321021c54)