affaan-m/ECC · error · Error
Invalid ECC repo root: unreadable package.json at
Error message
Invalid ECC repo root: unreadable package.json at ${packageJsonPath} What it means
After successfully opening the file, readRegularTextFile re-stats the path with lstat and compares identity (via sameFileIdentity — inode must match) against the opened descriptor. If the path has been swapped for a symlink, stopped being a regular file, or now refers to a different inode, it throws this error. This is a TOCTOU defense: it guarantees the file was not replaced between open and read.
Solutions
- Retry the read — the error usually indicates a transient concurrent replacement, and the next attempt will see a stable regular file.
- Stop processes that mutate the file during reads (editors with atomic save, sync clients, concurrent writers).
- Serialize vault access: use locking or ensure only one process reads/writes a memory file at a time.
- Investigate for tampering if the swap was not caused by a known local process — inode change to a symlink is an attack signature.
Example fix
// before (racy)
const data = readRegularTextFile(vaultFile, { label: 'memory' }); // throws if editor renames over it
// after (retry transient swap)
function readStable(file, opts) {
try { return readRegularTextFile(file, opts); }
catch (e) {
if (/must remain a regular/.test(e.message)) return readRegularTextFile(file, opts); // one retry
throw e;
}
} Defensive patterns
Strategy: retry
Validate before calling
// Cannot be pre-validated reliably (TOCTOU) — mitigate by stabilizing the file:
const before = fs.lstatSync(path);
// ... ensure no writers are active (flock or app-level lock) before reading
if (fs.lstatSync(path).ino !== before.ino) throw new Error('File changed during read setup; retry'); Type guard
const isStableRegularFile = (p) => {
const a = fs.lstatSync(p), b = fs.statSync(p);
return a.isFile() && !a.isSymbolicLink() && a.ino === b.ino;
}; Try / catch
const MAX_TRIES = 3;
for (let i = 0; ; i++) {
try { return readRegularTextFile(file, { label: 'memory document', maxBytes }); }
catch (err) {
if (err.message.includes('must remain a regular') && i < MAX_TRIES - 1) {
await new Promise(r => setTimeout(r, 50 * (i + 1))); // backoff, file was swapped mid-read
continue;
}
throw err;
}
} Prevention
- Serialize vault file access with file locks or a single-writer discipline.
- Disable/avoid atomic-save editors and sync clients while batch-reading the vault.
- Retry reads with short backoff — swaps are usually transient.
- Escalate to a security review if inode swaps recur without a known cause.
When it happens
Trigger: A concurrent process replaces or unlinks-and-recreates the file (especially with a symlink) after readRegularTextFile opens it but before/while reading; an editor's atomic-save (write temp + rename) races with the read; an attacker swaps the path mid-read.
Common situations: Editors or build tools performing atomic saves while a memory read is in flight; sync clients (Dropbox, iCloud) churning files; parallel test runs mutating the same vault file; active tampering attempting to redirect the read.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- artifact changed before reading
- artifact changed during reading
- Invalid ECC repo root: missing install script at
- Legacy sync path changed before removal
- Legacy sync path changed before removal; preserved…
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/79d3c01e282c3174.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/auto-update.js:155
function validateRepoRoot(repoRoot) {
const normalized = path.resolve(repoRoot);
const packageJsonPath = path.join(normalized, 'package.json');
const installApplyPath = path.join(normalized, 'scripts', 'install-apply.js');
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`Invalid ECC repo root: missing package.json at ${packageJsonPath}`);
}
if (!fs.existsSync(installApplyPath)) {
throw new Error(`Invalid ECC repo root: missing install script at ${installApplyPath}`);
}
let pkgName = null;
try {
pkgName = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).name;
} catch {
throw new Error(`Invalid ECC repo root: unreadable package.json at ${packageJsonPath}`);
}
if (!ECC_PACKAGE_NAMES.has(pkgName)) {
throw new Error(`Refusing to run install from untrusted repo root ${normalized}: package.json name '${pkgName}' is not an official ECC package.`);
}
return normalized;
}
function runExternalCommand(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd,
env: options.env || process.env,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024
});
if (result.error) {
throw result.error;View on GitHub (pinned to 8321021c54)