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

  1. Retry the read — the error usually indicates a transient concurrent replacement, and the next attempt will see a stable regular file.
  2. Stop processes that mutate the file during reads (editors with atomic save, sync clients, concurrent writers).
  3. Serialize vault access: use locking or ensure only one process reads/writes a memory file at a time.
  4. 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

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


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)