{"record":{"id":"71cfaf01579b4208","repo":"affaan-m/ECC","slug":"unsafe-state-store-path-targetpath-a-symlink-is-not-allowed","errorCode":null,"errorMessage":"Unsafe state-store path '${targetPath}': a symlink is not allowed","messagePattern":"Unsafe state-store path '(.+?)': a symlink is not allowed","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/state-store/index.js","lineNumber":67,"sourceCode":"  ]);\n  const expectedTarget = allowedTargets.get(targetPath);\n  if (!expectedTarget) {\n    return false;\n  }\n\n  try {\n    return fs.realpathSync(targetPath) === expectedTarget;\n  } catch (_error) {\n    return false;\n  }\n}\n\nfunction assertNotSymlink(targetPath, stats) {\n  if (stats && stats.isSymbolicLink()) {\n    if (isAllowedPlatformSymlink(targetPath, stats)) {\n      return;\n    }\n    throw stateStorePathError(targetPath, 'a symlink is not allowed');\n  }\n}\n\nfunction ensurePrivateDirectory(directoryPath) {\n  const absolutePath = path.resolve(directoryPath);\n  const parsed = path.parse(absolutePath);\n  const segments = absolutePath.slice(parsed.root.length).split(path.sep).filter(Boolean);\n  let currentPath = parsed.root;\n\n  for (const segment of segments) {\n    currentPath = path.join(currentPath, segment);\n    let stats = lstatIfPresent(currentPath);\n    assertNotSymlink(currentPath, stats);\n\n    if (!stats) {\n      try {\n        fs.mkdirSync(currentPath, { mode: PRIVATE_DIRECTORY_MODE });\n      } catch (error) {","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/state-store/index.js#L49-L85","documentation":"assertNotSymlink in scripts/lib/state-store/index.js guards the ECC state store (state.db and its parent directories) against symlink-based attacks: it lstats every path component and throws 'Unsafe state-store path ... a symlink is not allowed' when any component is a symlink, unless it is a macOS-owned /var|/tmp|/etc → /private/* alias. The store's DB contents are treated as sensitive, so symlinked paths are rejected outright.","triggerScenarios":"Opening/initializing the state store calls ensurePrivateDirectory(dirPath) (any path component, including ~/.claude, is a symlink) or assertSafeDatabaseFile(dbPath) (the state.db file itself is a symlink), e.g. when ECC_STATE_STORE path or ~/.claude was replaced by a symlink.","commonSituations":"User symlinked ~/.claude into a Dropbox/ synced folder to share settings; dotfile managers (stow, chezmoi, GNU stow farms) replaced .claude with a link; CI containers bind-mounting a symlinked cache directory; moving the DB with ln -s instead of an env/config path.","solutions":["Replace the symlink with a real directory: rm the link, mkdir the path, and copy contents back (rsync -a link/ realdir/), then retry.","Point ECC at the real location via its state-store path configuration instead of symlinking, keeping every component a physical directory.","On macOS, only the system /var, /tmp, /etc → /private/* aliases are allowed; any user symlink must be removed regardless of target.","Reconfigure your dotfile manager to manage the contents of ~/.claude rather than the directory itself."],"exampleFix":"// before: symlinked state dir\nln -s ~/Dropbox/claude ~/.claude\n// -> Unsafe state-store path '~/.claude': a symlink is not allowed\n\n// after: real directory, sync contents instead\nrm ~/.claude\nmkdir ~/.claude\nrsync -a ~/Dropbox/claude/ ~/.claude/","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nconst path = require('path');\nfunction hasNoSymlinkComponents(p) {\n  let cur = path.parse(path.resolve(p)).root;\n  for (const seg of path.resolve(p).split(path.sep).filter(Boolean)) {\n    cur = path.join(cur, seg);\n    const st = fs.lstatSync(cur, { throwIfNoEntry: false });\n    if (st && st.isSymbolicLink()) return false;\n  }\n  return true;\n}","typeGuard":"function isRealDirectory(p) {\n  try { const st = fs.lstatSync(p); return st.isDirectory() && !st.isSymbolicLink(); }\n  catch { return false; }\n}","tryCatchPattern":"try {\n  openStateStore(dbPath);\n} catch (e) {\n  if (String(e.message).includes('a symlink is not allowed')) {\n    console.error('State-store path contains a symlink:', e.message);\n    console.error('Replace it with a real directory (see ECC state-store path config).');\n  } else throw e;\n}","preventionTips":["Never symlink ~/.claude or any parent of state.db (dotfile managers: link files, not the directory).","Configure ECC's state-store path to point at a real directory instead of relocating the DB with ln -s.","After dotfile/bootstrap scripts run, lstat -check that ~/.claude is a real directory.","On macOS, expect only /var, /tmp, /etc system aliases to be accepted."],"tags":["symlink","security","path-validation","state-store"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}