affaan-m/ECC · error · Error
${label} must remain a regular, non-symlink file while it is
Error message
${label} must remain a regular, non-symlink file while it is opened. What it means
After successfully opening a memory file, readRegularTextFile re-runs lstat on the path and compares identity (inode via sameFileIdentity) against the descriptor's fstat. If the path now resolves to a symlink, a non-regular file, or a different inode, the open and the on-disk state have diverged — a classic TOCTOU swap. The vault treats this as an attack signal and aborts rather than read possibly-attacker-controlled content.
Source
Thrown at scripts/lib/memory-vault.js:164
assertWithinTrustedRoot(filePath, options.trustedRoot, `read ${label}`);
}
const flags = fs.constants.O_RDONLY
| (fs.constants.O_NOFOLLOW || 0)
| (fs.constants.O_NONBLOCK || 0);
const descriptor = fs.openSync(filePath, flags);
try {
const opened = fs.fstatSync(descriptor, { bigint: true });
if (!opened.isFile()) {
throw new Error(`${label} must be a regular, non-symlink file.`);
}
const after = fs.lstatSync(filePath, { bigint: true });
if (
after.isSymbolicLink()
|| !after.isFile()
|| !sameFileIdentity(after, opened)
) {
throw new Error(`${label} must remain a regular, non-symlink file while it is opened.`);
}
if (options.trustedRoot) {
assertWithinTrustedRoot(filePath, options.trustedRoot, `read ${label}`);
}
if (opened.size > BigInt(maxBytes)) {
throw new Error(`${label} is too large (${opened.size} bytes).`);
}
const chunks = [];
let total = 0;
while (total <= maxBytes) {
const buffer = Buffer.alloc(Math.min(64 * 1024, maxBytes + 1 - total));
const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, null);
if (bytesRead === 0) break;
chunks.push(buffer.subarray(0, bytesRead));
total += bytesRead;
}
if (total > maxBytes) {View on GitHub (pinned to 01e15490f0)
Solutions
- Retry the operation when no other process is touching the vault (avoid concurrent saves to the same id).
- On Windows, upgrade Node to 22.17+ or 24.2+ (libuv 1.51.0+ contains the fix referenced in the sameFileIdentity comment).
- Pause any sync tool (Syncthing, Dropbox, Maestral) that targets the vault directory and re-run the read.
- If the file is genuinely being swapped by a legitimate pipeline, serialize vault access behind a lock so reads and writes do not overlap.
Example fix
// before: concurrent processes both writing mem_x.md, reads intermittently fail
// serialize with a lockfile
const properLockfile = require('proper-lockfile');
await properLockfile.lock(vaultRoot);
try { await readMemoryById('mem_x'); } finally { await properLockfile.unlock(vaultRoot); }
// after: no in-flight swaps, identity check passes Defensive patterns
Strategy: retry
Validate before calling
const fs = require('fs');
function stableIdentity(p) {
const a = fs.statSync(p, { bigint: true });
const b = fs.statSync(p, { bigint: true });
return a.ino === b.ino && (a.dev === b.dev);
}
// only a hint; the real guard is serializing access so the file does not change mid-read Try / catch
async function readStable(id, retries = 2) {
for (let i = 0; i <= retries; i++) {
try { return readMemoryById(id); }
catch (error) {
if (/remain a regular, non-symlink file/i.test(error.message) && i < retries) continue;
throw error;
}
}
} Prevention
- Serialize concurrent vault access with a lock (proper-lockfile, lockfile, or a queue).
- On Windows, use Node 22.17+ / 24.2+ (libuv 1.51.0+) to avoid the volume-serial stat mismatch.
- Do not run sync tools that rewrite vault files mid-scan.
- Treat intermittent 'remain a regular file' errors as a signal of concurrency, not a bug in the vault.
When it happens
Trigger: Between fs.openSync and the subsequent fs.lstatSync, another process replaces the file (rename/link/unlink) so the path no longer points at the originally-opened inode. Realistically only seen under concurrent modification, a hostile local process, a misbehaving sync tool that rewrote the file mid-read, or on Windows with affected libuv versions (1.49.0–1.50.x) where stat/lstat and fstat disagree on volume serial.
Common situations: Two agents or two CLI invocations writing the same memory file at once; an editor or sync engine rewriting the file during a vault scan; downgrading/upgrading Node across the 22.12–22.16 or 24.0–24.1 window on Windows where the libuv GetFileInformationByName bug left volume serial unset; a security tool quarantining and replacing the file mid-read.
Related errors
- Memory destination changed while it was being created.
- Refusing to access memory through symlink root: ${root}
- Refusing to access memory through symlink directory: ${direc
- ${label} is too large (maximum ${maxBytes} bytes).
- Refusing to ${action}: destination parent is not a trusted d
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/5e32e46b37708e3b.
Report an issue: GitHub.