affaan-m/ECC · critical · Error
Invalid ECC repo root: missing package.json at
Error message
Invalid ECC repo root: missing package.json at ${packageJsonPath} What it means
assertMemoryDirectorySafe applies the same symlink check that the root gets, but to a subdirectory of the vault (asserting it stays within the trusted root). If the directory exists and lstat reports a symbolic link, the library refuses to traverse it. This blocks symlink-based escapes from inside the vault (e.g. an attacker planting 'project -> /etc' inside the memory directory).
Solutions
- Replace the symlinked directory with a real directory and copy/move its contents in place.
- Audit the vault tree for unexpected links (`find ~/.ecc-memory -type l`) and remove any you did not create.
- Point the whole scope root config at the real location if you intentionally keep the data elsewhere.
- Re-check directory contents after any restore/sync operation that may have reintroduced symlinks.
Example fix
// before (shell) ln -s /shared/notes ~/.ecc-memory/project/notes // after (shell) rm ~/.ecc-memory/project/notes && mkdir ~/.ecc-memory/project/notes && cp -rL /shared/notes/. ~/.ecc-memory/project/notes/
Defensive patterns
Strategy: validation
Validate before calling
const fs = require('fs');
function assertNoSymlinkedSubdirs(vaultRoot) {
for (const entry of fs.readdirSync(vaultRoot)) {
const p = `${vaultRoot}/${entry}`;
if (fs.lstatSync(p).isSymbolicLink()) {
throw new Error(`Symlinked directory inside vault: ${p}. Replace with a real directory.`);
}
}
} Type guard
const isRealDirectory = (p) => { try { const st = fs.lstatSync(p); return st.isDirectory() && !st.isSymbolicLink(); } catch { return false; } }; Try / catch
try {
saveMemory(scope, doc);
} catch (err) {
if (err.message.includes('symlink directory')) {
console.error(`Security: replace the symlinked vault subdirectory with a real one: ${err.message}`);
} else throw err;
} Prevention
- Audit vault subdirectories for symlinks after restores, archive extractions, or sync operations.
- Extract vault archives with symlink preservation disabled.
- Restrict write access to the vault directory so untrusted processes cannot plant links.
- Use `find <vault> -type l` in a scheduled check and alert on any result.
When it happens
Trigger: Calling saveMemory or directory-listing paths when a scope subdirectory (e.g. the per-scope memory directory under the root) is a symlink. Triggered by directories/saveMemory callers whenever the checked directory path exists as a link.
Common situations: A user symlinked a subfolder of the vault to shared storage; a compromised process planted a symlink inside the vault; restoring from an archive that preserved symlinks; a package manager or tool replaced a vault subdirectory with a link during an update.
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
- Unable to infer ECC repo root from install-state operations
- Refusing to reuse unsafe legacy Codex ownership path
- artifact path escapes output directory
- gate.variant_invalid
- Invalid ECC repo root: missing install script at
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/3c9980d0e5503fcd.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/auto-update.js:144
return path.dirname(record.state.target.root);
}
return repoRoot;
}
// Recognized ECC package names. A repo root is only trusted to run its
// install-apply.js if its package.json identifies it as ECC — otherwise a
// cloned project that ships a nested `evil/{package.json,scripts/install-apply.js}`
// could drive auto-update into executing attacker code (GHSA-hfpv-w6mp-5g95).
const ECC_PACKAGE_NAMES = new Set(['ecc-universal', 'everything-claude-code']);
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;
}View on GitHub (pinned to 8321021c54)