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
- 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.
- Run doctorMemoryVault (or the doctor command) to confirm no other kind directories are symlinks.
- Re-initialize the vault with initializeVault after removing the offending symlinks so the directory structure is recreated cleanly.
- 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
- Never symlink kind directories (notes/, decisions/) to share content between scopes.
- After restoring from backup, confirm the vault tree has no symlinks: find <root> -type l.
- Exclude the vault from file-level sync tools that may convert directories to symlinks.
- Periodically run doctorMemoryVault to surface skipped symlinks.
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
- Refusing to access memory through symlink root: ${root}
- Refusing to ${action}: destination parent is not a trusted d
- ${label} must remain a regular, non-symlink file while it is
- Memory destination changed while it was being created.
- Refusing to read non-file path: ${filePath}
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/6f5f2b0e5c36dc70.
Report an issue: GitHub.