{"record":{"id":"66d56448fc768481","repo":"affaan-m/ECC","slug":"refusing-to-action-destination-parent-is-not-a","errorCode":null,"errorMessage":"Refusing to ${action}: destination parent is not a trusted directory.","messagePattern":"Refusing to (.+?): destination parent is not a trusted directory\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/install-lifecycle.js","lineNumber":403,"sourceCode":"    ? relativeParent.split(path.sep).filter(Boolean)\n    : [];\n  let currentPath = canonicalRoot;\n\n  for (const segment of pathSegments) {\n    const validatedParent = assertWithinTrustedRoot(currentPath, canonicalRoot, action);\n    const nextPath = path.join(validatedParent, segment);\n    try {\n      fs.mkdirSync(nextPath);\n    } catch (error) {\n      if (!error || error.code !== 'EEXIST') {\n        throw error;\n      }\n    }\n\n    const validatedNext = assertWithinTrustedRoot(nextPath, canonicalRoot, action);\n    const nextStat = fs.lstatSync(validatedNext);\n    if (!nextStat.isDirectory() || nextStat.isSymbolicLink()) {\n      throw new Error(`Refusing to ${action}: destination parent is not a trusted directory.`);\n    }\n    currentPath = validatedNext;\n  }\n\n  return getManagedDestination(managedPath, canonicalRoot, action).managedPath;\n}\n\nfunction prepareContainedWriteDestination(destinationPath, trustedRoot, action) {\n  return ensureContainedParentDir(destinationPath, trustedRoot, action);\n}\n\nfunction getContainedExistingPath(\n  destinationPath,\n  trustedRoot,\n  action,\n  { allowFinalSymlink = false } = {}\n) {\n  const initialDestination = getManagedDestination(","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/install-lifecycle.js#L385-L421","documentation":"ensureContainedWriteDestination walks each parent directory segment between the trusted root and the destination, verifying via lstat that every intermediate component is a real directory and not a symbolic link. If a component is a symlink or otherwise not a directory, the write is refused to prevent symlink-based escapes from the trusted root (defense against malicious install state).","triggerScenarios":"The destination path contains a parent component that exists on disk as a symbolic link, or as a regular file, while preparing a contained write — e.g. ~/.claude/agents being a symlink to another location, or a path segment replaced by a file during a concurrent operation.","commonSituations":"Users symlink ~/.claude (or a subdirectory like agents/, skills/, hooks/) into a dotfiles repo or another volume; sync tools (Dropbox, OneDrive) replacing directories with junctions/symlinks; an attacker or corrupted state swapping a directory component for a symlink mid-install.","solutions":["Replace the symlinked parent directory with a real directory (copy contents back) or point the destination inside the real target location.","Verify which path component is the problem with `ls -la` along the destination path and remove/recreate it as a genuine directory.","Update the install-state destinationPath to a location that does not traverse any symlinks under the trusted root.","Re-run the install after fixing the filesystem layout; if a tool keeps recreating the symlink, reconfigure that tool."],"exampleFix":"# before: ~/.claude/agents -> ~/dotfiles/agents (symlink)\n# after\nrm ~/.claude/agents\nmkdir ~/.claude/agents\ncp -r ~/dotfiles/agents/. ~/.claude/agents/","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nfunction assertNoSymlinkParents(destPath, root) {\n  let cur = require('path').dirname(require('path').resolve(destPath));\n  const stop = require('path').resolve(root);\n  while (cur.startsWith(stop)) {\n    const st = fs.lstatSync(cur);\n    if (st.isSymbolicLink() || !st.isDirectory()) {\n      throw new Error(`Parent is a symlink or not a directory: ${cur}`);\n    }\n    const parent = require('path').dirname(cur);\n    if (parent === cur) break;\n    cur = parent;\n  }\n}","typeGuard":"function isRealDirectory(p) {\n  try { const st = fs.lstatSync(p); return st.isDirectory() && !st.isSymbolicLink(); } catch { return false; }\n}","tryCatchPattern":"try {\n  await install(operations);\n} catch (err) {\n  if (/destination parent is not a trusted directory/.test(err.message)) {\n    console.error('Replace the symlinked parent directory with a real directory, then re-run.');\n  } else throw err;\n}","preventionTips":["Do not symlink ~/.claude or its subdirectories into dotfiles repos; use real directories and a dotfiles manager that copies.","Exclude the install directories from sync tools that create junctions/symlinks.","Check `ls -la` along the destination path before installing on a new machine."],"tags":["symlink","security","path"],"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"}