affaan-m/ECC · error · Error

Refusing to access memory through symlink directory: ${direc

Error message

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

What it means

The per-kind memory subdirectory (e.g. <root>/notes, <root>/decisions) is itself a symbolic link, and the vault refuses to traverse it. assertMemoryDirectorySafe guards the kind directories created under each scope root and rejects symlinks for the same TOCTOU reason as the root check: a symlinked subdirectory could be swapped to redirect writes outside the trusted boundary between the boundary check and the actual file write.

Source

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

  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
  // through GetFileInformationByName, which leaves the volume serial unset, while
  // fstat() on an open handle reports it. Comparing the two then never matches and
  // every vault read and write is rejected. libuv 82cdfb75f fixed this in 1.51.0,
  // so only Node 22.12-22.16 and 24.0-24.1 are affected, but the guard should not
  // depend on the runtime's patch level. Compare dev only when both sides report
  // one; POSIX always does, so the original strict behaviour is preserved there.
  if (!left.dev || !right.dev) {
    return true;

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Inspect the offending directory path reported in the message and replace the symlink with a real directory: rm <directory> && mkdir -p <directory>, then move the target contents in.
  2. Run doctorMemoryVault (or the doctor command) to confirm no other kind directories are symlinks.
  3. Re-initialize the vault with initializeVault after removing the offending symlinks so the directory structure is recreated cleanly.
  4. Audit any sync/backup tool pointed at the vault and configure it to copy files rather than create symlinks.

Example fix

// before: .ecc/memory/project/notes -> /shared/notes (symlink)
// shell fix:
//   rm .ecc/memory/project/notes
//   mkdir -p .ecc/memory/project/notes
//   cp -a /shared/notes/. .ecc/memory/project/notes/
// after: .ecc/memory/project/notes is a real directory
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
const path = require('path');
function assertKindDirSafe(root, kind) {
  const dir = path.join(root, `${kind}s`);
  if (fs.existsSync(dir) && fs.lstatSync(dir).isSymbolicLink()) {
    throw new Error(`Refusing to access memory through symlink directory: ${dir}`);
  }
  return dir;
}

Type guard

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

Try / catch

try { initializeVault({ scopes: ['project'] }); }
catch (error) {
  if (/symlink directory/.test(error.message)) {
    console.error('A kind subdirectory is a symlink; replace it:', error.message);
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: Calling initializeVault or saveMemory when path.join(root, kind + 's') (e.g. .ecc/memory/project/notes) exists and is a symbolic link. Occurs when someone manually symlinks a kind directory, a restore/merge script created symlinks instead of copying, or a partial migration left symlinks pointing at an old vault layout.

Common situations: Manual reorganization of the vault where a user symlinked notes/ to a shared folder; backup restore tools that recreate directory trees with symlinks; merging two vaults by symlinking kind directories; bugs in external sync tools (Syncthing, Maestral) that convert directories to symlinks under pressure.

Related errors


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