{"record":{"id":"aabb5ff211e477e2","repo":"paperclipai/paperclip","slug":"refusing-to-use-non-directory-install-store-path","errorCode":null,"errorMessage":"Refusing to use non-directory install-store path ${directoryPath}.","messagePattern":"Refusing to use non-directory install-store path (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"cli/src/install-store.ts","lineNumber":45,"sourceCode":"  previous: InstallRecord[];\n};\n\nexport type InstallStorePaths = {\n  paperclipHome: string;\n  cliRoot: string;\n  installsRoot: string;\n  manifestPath: string;\n  markerPath: string;\n  lockPath: string;\n  currentPath: string;\n  shimPath: string;\n};\n\nfunction ensurePrivateDirectory(directoryPath: string): void {\n  fs.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });\n  const stat = fs.lstatSync(directoryPath);\n  if (!stat.isDirectory() || stat.isSymbolicLink()) {\n    throw new Error(`Refusing to use non-directory install-store path ${directoryPath}.`);\n  }\n  fs.chmodSync(directoryPath, 0o700);\n}\n\nfunction assertOwnedByCurrentUser(stat: fs.Stats, targetPath: string): void {\n  const getuid = process.getuid;\n  if (typeof getuid === \"function\" && stat.uid !== getuid()) {\n    throw new Error(`Refusing to modify path not owned by the current user: ${targetPath}.`);\n  }\n}\n\nfunction writeFileAtomic(filePath: string, contents: string, mode: number): void {\n  const temporaryPath = path.join(\n    path.dirname(filePath),\n    `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,\n  );\n  try {\n    fs.writeFileSync(temporaryPath, contents, { mode, flag: \"wx\" });","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/install-store.ts#L27-L63","documentation":"Thrown by ensurePrivateDirectory() inside install-store.ts when a path that must be a real private directory is, after mkdirSync + lstatSync, found to be either not a directory or a symbolic link. The install store requires real 0o700 directories to prevent symlink-based path-confusion attacks where an attacker redirects the store into an arbitrary location.","triggerScenarios":"Called initializeInstallStore(), assertManagedInstallStore(), or any flow that calls ensurePrivateDirectory on cliRoot or installsRoot, where the path is occupied by a regular file or a symlink (lstatSync().isSymbolicLink() is true, or isDirectory() is false).","commonSituations":"1) A symlink was placed at ~/.paperclip/cli pointing elsewhere (accidental or malicious). 2) A previous failed install left a regular file where a directory is expected. 3) A dotfile manager or backup restore turned the directory into a symlink. 4) Filesystem corruption or a bad mount.","solutions":["Inspect the offending path with 'ls -la' and confirm what type of entry it is.","If it is a symlink or stray file you created, remove it ('rm <path>') and let the CLI recreate the directory.","If the symlink is unexpected and you did not create it, investigate for tampering before removing — do not blindly delete.","Move any needed data out of the path first if it contains real install payloads."],"exampleFix":"// before: ~/.paperclip/cli is a symlink\n$ ls -la ~/.paperclip/cli\nlrwxrwxrwx  cli -> /tmp/evil\n\n// after: remove symlink, let CLI recreate as real dir\n$ rm ~/.paperclip/cli\n$ paperclipai install ...","handlingStrategy":"validation","validationCode":"import fs from \"node:fs\";\n\nfunction isRealPrivateDirectory(p: string): boolean {\n  try {\n    const st = fs.lstatSync(p);\n    return st.isDirectory() && !st.isSymbolicLink();\n  } catch { return false; }\n}\n\n// Before calling install-store ops:\nif (!isRealPrivateDirectory(cliRoot)) { /* remove stray entry or repoint */ }","typeGuard":"import fs from \"node:fs\";\n\nfunction isSafeDirectory(p: string): boolean {\n  const st = fs.lstatSync(p);\n  return st.isDirectory() && !st.isSymbolicLink();\n}","tryCatchPattern":"try {\n  initializeInstallStore(paths);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"non-directory install-store path\")) {\n    // surface to user, ask before removing the stray entry\n    throw err;\n  }\n  throw err;\n}","preventionTips":["Never symlink ~/.paperclip/cli or its installs subdirectory.","Audit the store directory after restore-from-backup operations.","Avoid dotfile managers that symlink the .paperclip tree.","If mounting the store on a network filesystem, ensure it presents real directories."],"tags":["install-store","security","symlink-guard","filesystem","cli"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}