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

  1. Remove the symlink (rm the link) and create a real directory at the root path, moving the data into it.
  2. Update the roots config to point directly at the real (non-symlink) target directory instead of linking.
  3. Investigate how the symlink appeared if you did not create it — treat it as a potential tampering signal.
  4. 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

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


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)