{"record":{"id":"6f5f2b0e5c36dc70","repo":"affaan-m/ECC","slug":"refusing-to-access-memory-through-symlink-director","errorCode":null,"errorMessage":"Refusing to access memory through symlink directory: ${directory}","messagePattern":"Refusing to access memory through symlink directory: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/memory-vault.js","lineNumber":119,"sourceCode":"  const root = roots[scope];\n  if (typeof root !== 'string' || root.length === 0) {\n    throw new Error(`No memory root is configured for scope \"${scope}\".`);\n  }\n  const boundary = roots[VAULT_ROOT_BOUNDARIES]?.[scope];\n  if (typeof boundary !== 'string' || boundary.length === 0) {\n    throw new Error(`No trusted boundary policy is configured for memory scope \"${scope}\".`);\n  }\n  assertWithinTrustedRoot(root, boundary, 'access memory through a symlink');\n  if (fs.existsSync(root) && fs.lstatSync(root).isSymbolicLink()) {\n    throw new Error(`Refusing to access memory through symlink root: ${root}`);\n  }\n  return root;\n}\n\nfunction assertMemoryDirectorySafe(directory, root) {\n  assertWithinTrustedRoot(directory, root, 'access memory directory');\n  if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink()) {\n    throw new Error(`Refusing to access memory through symlink directory: ${directory}`);\n  }\n  return directory;\n}\n\nfunction sameFileIdentity(left, right) {\n  // The inode is the primary identity signal and must always match.\n  if (left.ino !== right.ino) {\n    return false;\n  }\n  // libuv 1.49.0 through 1.50.x resolve path-based stat() and lstat() on Windows\n  // through GetFileInformationByName, which leaves the volume serial unset, while\n  // fstat() on an open handle reports it. Comparing the two then never matches and\n  // every vault read and write is rejected. libuv 82cdfb75f fixed this in 1.51.0,\n  // so only Node 22.12-22.16 and 24.0-24.1 are affected, but the guard should not\n  // depend on the runtime's patch level. Compare dev only when both sides report\n  // one; POSIX always does, so the original strict behaviour is preserved there.\n  if (!left.dev || !right.dev) {\n    return true;","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/memory-vault.js#L101-L137","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: .ecc/memory/project/notes -> /shared/notes (symlink)\n// shell fix:\n//   rm .ecc/memory/project/notes\n//   mkdir -p .ecc/memory/project/notes\n//   cp -a /shared/notes/. .ecc/memory/project/notes/\n// after: .ecc/memory/project/notes is a real directory","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nconst path = require('path');\nfunction assertKindDirSafe(root, kind) {\n  const dir = path.join(root, `${kind}s`);\n  if (fs.existsSync(dir) && fs.lstatSync(dir).isSymbolicLink()) {\n    throw new Error(`Refusing to access memory through symlink directory: ${dir}`);\n  }\n  return dir;\n}","typeGuard":"function isNonSymlinkDir(p) {\n  return !fs.existsSync(p) || (fs.lstatSync(p).isDirectory() && !fs.lstatSync(p).isSymbolicLink());\n}","tryCatchPattern":"try { initializeVault({ scopes: ['project'] }); }\ncatch (error) {\n  if (/symlink directory/.test(error.message)) {\n    console.error('A kind subdirectory is a symlink; replace it:', error.message);\n    return;\n  }\n  throw error;\n}","preventionTips":["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."],"tags":["security","symlink","memory-vault","filesystem","toctou"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}