affaan-m/ECC · error
Refusing to read a non-file at
Error message
Refusing to read a non-file at ${filePath} What it means
hashFileNoFollow() opens a file with O_NOFOLLOW (refusing to follow symlinks) and verifies via fstat that the opened descriptor is a regular file before reading it. If the path is not a regular file (directory, FIFO, device, or a filesystem without O_NOFOLLOW resolution), it throws this error instead of hashing it. This is a hardening measure so legacy-migration verification never reads through symlinks or special files.
Solutions
- Replace the symlink (or non-file) at the path with a real regular file copy: `cp --remove-destination <target> <path>`.
- If your dotfile manager symlinks this file, configure it to copy real files for ECC-managed paths.
- Verify what is at the path with `ls -la` / `file <path>` to confirm whether it is a symlink, directory, or other non-file.
- If migration verification cannot proceed legitimately, remove the legacy file and re-run migration from a genuine source file.
Example fix
// before: path is a symlink ln -s ~/dotfiles/CLAUDE.md ~/.claude/CLAUDE.md // after: real file cp --remove-destination ~/dotfiles/CLAUDE.md ~/.claude/CLAUDE.md
Defensive patterns
Strategy: validation
Validate before calling
const fs = require('fs');
const st = fs.lstatSync(filePath);
if (!st.isFile()) {
throw new Error(`${filePath} is not a regular file (symlink/dir?) — replace with a real file`);
} Type guard
function isRegularFileNoFollow(p) {
try { return fs.lstatSync(p).isFile(); } catch { return false; }
} Try / catch
try {
const hash = hashFileNoFollow(filePath);
} catch (err) {
if (err.message.startsWith('Refusing to read a non-file')) {
console.error(`${filePath} is a symlink or special file; materialize a real file copy first`);
process.exitCode = 1;
} else throw err;
} Prevention
- Configure dotfile managers to copy (not symlink) ECC-managed files.
- Run ls -la over managed paths before migration to spot symlink substitutions.
- Never replace managed files with symlinks or FIFOs; keep them regular files.
- Re-run migration from genuine source files rather than pointing it at links.
When it happens
Trigger: Calling hashFileNoFollow() (via verifyManagedLegacyFile) on a path that is a symlink to a file, a directory, a FIFO/socket, or a device node; an attacker or misconfiguration replacing an expected managed file with a symlink; a platform where O_NOFOLLOW is unavailable so the opened inode turns out to be non-regular.
Common situations: Dotfile managers (stow, chezmoi, GNU stow-style farms) that symlink ~/.claude contents instead of copying them, so managed legacy files are symlinks; a directory accidentally placed where a file is expected; security tooling flagging symlink swaps during migration verification.
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 be a real directory, not a…
- output artifact must be a regular file
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/500de35277c56949.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/install/opencode-legacy-migration.js:107
return { status: 'invalid', state: null, error: null };
}
return { status: 'valid', state, error: null };
} catch (error) {
return {
status: 'unreadable',
state: null,
error: `Unable to inspect legacy OpenCode install-state at ${location.installStatePath}: ${error.message}`,
};
}
}
function hashFileNoFollow(filePath) {
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
const descriptor = fs.openSync(filePath, flags);
try {
const before = fs.fstatSync(descriptor, { bigint: true });
if (!before.isFile()) {
throw new Error(`Refusing to read a non-file at ${filePath}`);
}
const content = fs.readFileSync(descriptor);
const after = fs.fstatSync(descriptor, { bigint: true });
const finalPathStat = fs.lstatSync(filePath, { bigint: true });
const unchanged = before.dev === after.dev
&& before.ino === after.ino
&& before.size === after.size
&& before.mtimeMs === after.mtimeMs
&& before.ctimeMs === after.ctimeMs
&& after.dev === finalPathStat.dev
&& after.ino === finalPathStat.ino
&& after.size === finalPathStat.size
&& after.mtimeMs === finalPathStat.mtimeMs
&& after.ctimeMs === finalPathStat.ctimeMs;
if (finalPathStat.isSymbolicLink() || !finalPathStat.isFile() || !unchanged) {
throw new Error(`Refusing to read a file that changed during validation: ${filePath}`);
}
return {View on GitHub (pinned to 8321021c54)