{"record":{"id":"ee95066111e0b246","repo":"affaan-m/ECC","slug":"refusing-to-access-memory-through-symlink-root","errorCode":null,"errorMessage":"Refusing to access memory through symlink root: ${root}","messagePattern":"Refusing to access memory through symlink root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/memory-vault.js","lineNumber":111,"sourceCode":"  });\n  return Object.freeze(roots);\n}\n\nfunction assertMemoryRootSafe(roots, scope) {\n  if (!roots || typeof roots !== 'object' || Array.isArray(roots)) {\n    throw new Error('Memory roots must include a trusted boundary policy.');\n  }\n  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","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/memory-vault.js#L93-L129","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","solutions":["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.","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).","If a dotfile manager owns ~/.ecc, configure it to manage the directory contents (the files inside) rather than symlinking the directory itself.","Move the actual vault data onto the same filesystem as the expected root and stop indirection through a symlink."],"exampleFix":"// before: ECC_MEMORY_USER_ROOT=~/.ecc (symlink to /data/ecc)\n// resolve to the real path\nconst realRoot = fs.realpathSync(process.env.ECC_MEMORY_USER_ROOT);\nprocess.env.ECC_MEMORY_USER_ROOT = realRoot;\n// after: ECC_MEMORY_USER_ROOT=/data/ecc/memory (a real directory)","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nfunction assertRootNotSymlink(root) {\n  if (fs.existsSync(root) && fs.lstatSync(root).isSymbolicLink()) {\n    throw new Error(`Refusing to access memory through symlink root: ${root}`);\n  }\n  return fs.realpathSync(root);\n}\n// before calling saveMemory / readMemoryById / etc.:\nconst realRoot = assertRootNotSymlink(roots[scope]);","typeGuard":"function isNonSymlinkDirectory(p) {\n  return fs.existsSync(p) && fs.lstatSync(p).isDirectory() && !fs.lstatSync(p).isSymbolicLink();\n}","tryCatchPattern":"try { saveMemory(input); }\ncatch (error) {\n  if (/symlink root/.test(error.message)) {\n    console.error('Vault root is a symlink; replace it with a real directory:', error.message);\n    return;\n  }\n  throw error;\n}","preventionTips":["Never point ECC_MEMORY_PROJECT_ROOT or ECC_MEMORY_USER_ROOT at a symlinked path; resolve with realpath first.","Configure dotfile managers to manage vault contents, not symlink the vault directory itself.","After any vault migration, run doctorMemoryVault to confirm no roots are symlinks.","On CI, set HOME to a real directory rather than a symlinked temp path."],"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"}