affaan-m/ECC · error · Error

Refusing to access memory through symlink root: ${root}

Error message

Refusing to access memory through symlink root: ${root}

What it means

The memory vault refuses to read or write through a memory root directory that is itself a symbolic link. This is a hard security stop layered on top of the trusted-boundary check: even though the resolved root passed assertWithinTrustedRoot, the code treats a symlinked root as an unacceptable TOCTOU/symlink-swap risk because an attacker who controls the link target could redirect every subsequent vault read and write. It fires inside assertMemoryRootSafe, which guards every vault entry point (initializeVault, saveMemory, readMemoryFiles, searchMemories, readMemoryById, doctorMemoryVault).

Source

Thrown at scripts/lib/memory-vault.js:111

  });
  return Object.freeze(roots);
}

function assertMemoryRootSafe(roots, scope) {
  if (!roots || typeof roots !== 'object' || Array.isArray(roots)) {
    throw new Error('Memory roots must include a trusted boundary policy.');
  }
  const root = roots[scope];
  if (typeof root !== 'string' || root.length === 0) {
    throw new Error(`No memory root is configured for scope "${scope}".`);
  }
  const boundary = roots[VAULT_ROOT_BOUNDARIES]?.[scope];
  if (typeof boundary !== 'string' || boundary.length === 0) {
    throw new Error(`No trusted boundary policy is configured for memory scope "${scope}".`);
  }
  assertWithinTrustedRoot(root, boundary, 'access memory through a symlink');
  if (fs.existsSync(root) && fs.lstatSync(root).isSymbolicLink()) {
    throw new Error(`Refusing to access memory through symlink root: ${root}`);
  }
  return root;
}

function assertMemoryDirectorySafe(directory, root) {
  assertWithinTrustedRoot(directory, root, 'access memory directory');
  if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink()) {
    throw new Error(`Refusing to access memory through symlink directory: ${directory}`);
  }
  return directory;
}

function sameFileIdentity(left, right) {
  // The inode is the primary identity signal and must always match.
  if (left.ino !== right.ino) {
    return false;
  }
  // libuv 1.49.0 through 1.50.x resolve path-based stat() and lstat() on Windows

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Replace the symlinked vault root with a real directory: remove the symlink (rm <root>) and recreate it as a directory (mkdir -p <root>), then restore the contents.
  2. If you set ECC_MEMORY_PROJECT_ROOT or ECC_MEMORY_USER_ROOT, point them at the realpath of the target directory rather than a symlink (run readlink -f <path> and use that absolute path).
  3. If a dotfile manager owns ~/.ecc, configure it to manage the directory contents (the files inside) rather than symlinking the directory itself.
  4. Move the actual vault data onto the same filesystem as the expected root and stop indirection through a symlink.

Example fix

// before: ECC_MEMORY_USER_ROOT=~/.ecc (symlink to /data/ecc)
// resolve to the real path
const realRoot = fs.realpathSync(process.env.ECC_MEMORY_USER_ROOT);
process.env.ECC_MEMORY_USER_ROOT = realRoot;
// after: ECC_MEMORY_USER_ROOT=/data/ecc/memory (a real directory)
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
function assertRootNotSymlink(root) {
  if (fs.existsSync(root) && fs.lstatSync(root).isSymbolicLink()) {
    throw new Error(`Refusing to access memory through symlink root: ${root}`);
  }
  return fs.realpathSync(root);
}
// before calling saveMemory / readMemoryById / etc.:
const realRoot = assertRootNotSymlink(roots[scope]);

Type guard

function isNonSymlinkDirectory(p) {
  return fs.existsSync(p) && fs.lstatSync(p).isDirectory() && !fs.lstatSync(p).isSymbolicLink();
}

Try / catch

try { saveMemory(input); }
catch (error) {
  if (/symlink root/.test(error.message)) {
    console.error('Vault root is a symlink; replace it with a real directory:', error.message);
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: Calling any vault API for a scope whose resolved root (roots[scope], e.g. roots.project, roots.team, or roots.user) exists on disk and fs.lstatSync(root).isSymbolicLink() is true. This happens when ECC_MEMORY_PROJECT_ROOT or ECC_MEMORY_USER_ROOT resolves to a symlink, or when the default ~/.ecc/memory or .ecc/memory path is itself a symlink (common with dotfile managers, stow, or macOS /tmp redirections).

Common situations: Dotfile managers (stow, chezmoi, yadm) symlinking ~/.ecc into a repo; containers that bind-mount the vault through a symlinked volume; users moving ~/.ecc to another disk and symlinking it back; CI runners where HOME points at a symlinked temp dir; overriding ECC_MEMORY_PROJECT_ROOT to a path inside a symlinked workspace.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/ee95066111e0b246. Report an issue: GitHub.