affaan-m/ECC · critical · Error
Unable to infer ECC repo root from install-state operations
Error message
Unable to infer ECC repo root from install-state operations
What it means
Even with a root and boundary configured, assertMemoryRootSafe does a final runtime check: if the root path exists and lstat reports it is a symbolic link, it refuses to use it. This prevents an attacker from swapping the memory root for a symlink pointing outside the trusted boundary (e.g. to ~/.ssh or system files). The check uses lstat (not stat) so a link is detected before resolution.
Solutions
- Remove the symlink (rm the link) and create a real directory at the root path, moving the data into it.
- Update the roots config to point directly at the real (non-symlink) target directory instead of linking.
- Investigate how the symlink appeared if you did not create it — treat it as a potential tampering signal.
- If you legitimately need the data elsewhere, move the files and set the config path to the new location.
Example fix
// before (shell) ln -s ~/Dropbox/vault ~/.ecc-memory/project // after (shell) mv ~/Dropbox/vault ~/.ecc-memory/project # real directory, config points here directly
Defensive patterns
Strategy: validation
Validate before calling
const fs = require('fs');
function assertRealDirectory(path) {
if (fs.existsSync(path) && fs.lstatSync(path).isSymbolicLink()) {
throw new Error(`Refusing to use symlinked memory root: ${path}. Replace with a real directory.`);
}
} Type guard
const isRealDir = (p) => fs.existsSync(p) && fs.lstatSync(p).isDirectory() && !fs.lstatSync(p).isSymbolicLink();
Try / catch
try {
const root = resolveMemoryRoot(roots, scope);
} catch (err) {
if (err.message.includes('symlink root')) {
console.error(`Security: ${err.message}. Move the real data and point the config at it directly.`);
process.exitCode = 1;
} else throw err;
} Prevention
- Never create memory roots with `ln -s`; move data and update the config path instead.
- Run a periodic check (`find <vault> -type l`) to detect unexpected symlinks.
- Exclude the vault from cloud-sync tools that create links during restore.
- Treat an unexplained symlink as tampering and investigate before deleting it.
When it happens
Trigger: The configured scope root path resolves to a symlink — e.g. the vault directory was replaced by a link to another location, an attacker (or a syncing tool like Dropbox/git) created a link, or the user pre-created the root as 'ln -s /elsewhere vault' expecting it to work.
Common situations: Users symlink their memory vault into a dotfiles repo or cloud-sync folder; a restore tool recreated the directory as a symlink; a malicious or buggy script replaced the root to exfiltrate memory writes; moving the vault with `ln -s` instead of editing the config path.
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
- Invalid ECC repo root: missing package.json at
- 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/ae4a6de6f29e0389.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/auto-update.js:81
if (typeof operation.sourceRelativePath !== 'string' || !operation.sourceRelativePath.trim()) {
continue;
}
const relativeParts = operation.sourceRelativePath.split(/[\\/]+/).filter(Boolean);
if (relativeParts.length === 0) {
continue;
}
let repoRoot = path.resolve(operation.sourcePath);
for (let index = 0; index < relativeParts.length; index += 1) {
repoRoot = path.dirname(repoRoot);
}
return repoRoot;
}
throw new Error('Unable to infer ECC repo root from install-state operations');
}
function buildInstallApplyArgs(record) {
const state = record.state;
const target = state.target.target || record.adapter.target;
const request = state.request || {};
const args = [];
const hookConsent = getRecordedHookConsent(state);
if (target) {
args.push('--target', target);
}
if (request.profile) {
args.push('--profile', request.profile);
}
if (Array.isArray(request.modules) && request.modules.length > 0) {View on GitHub (pinned to 8321021c54)