{"record":{"id":"26c5352700d4df47","repo":"paperclipai/paperclip","slug":"refusing-to-activate-non-directory-payload-paylo","errorCode":null,"errorMessage":"Refusing to activate non-directory payload ${payloadPath}.","messagePattern":"Refusing to activate non-directory payload (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"cli/src/install-store.ts","lineNumber":254,"sourceCode":"): void {\n  ensurePrivateDirectory(paths.cliRoot);\n  const temporaryPath = `${paths.manifestPath}.tmp-${process.pid}-${Date.now()}`;\n  try {\n    fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\\n`, { mode: 0o600 });\n    fs.renameSync(temporaryPath, paths.manifestPath);\n  } finally {\n    fs.rmSync(temporaryPath, { force: true });\n  }\n}\n\nfunction assertPayloadPath(payloadPath: string, paths: InstallStorePaths): void {\n  const relative = path.relative(paths.installsRoot, path.resolve(payloadPath));\n  if (!relative || relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n    throw new Error(`Refusing to activate payload outside ${paths.installsRoot}.`);\n  }\n  const stat = fs.lstatSync(payloadPath);\n  if (!stat.isDirectory() || stat.isSymbolicLink()) {\n    throw new Error(`Refusing to activate non-directory payload ${payloadPath}.`);\n  }\n  const installsRealPath = fs.realpathSync(paths.installsRoot);\n  const payloadRealPath = fs.realpathSync(payloadPath);\n  if (!payloadRealPath.startsWith(`${installsRealPath}${path.sep}`)) {\n    throw new Error(`Refusing to activate payload that resolves outside ${paths.installsRoot}.`);\n  }\n}\n\nexport function flipCurrentAtomic(\n  payloadPath: string,\n  paths = resolveInstallStorePaths(),\n  hooks: { beforeRename?: () => void } = {},\n): void {\n  assertPayloadPath(payloadPath, paths);\n  ensurePrivateDirectory(paths.cliRoot);\n  try {\n    const currentStat = fs.lstatSync(paths.currentPath);\n    if (!currentStat.isSymbolicLink()) {","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/install-store.ts#L236-L272","documentation":"Thrown by assertPayloadPath() when the payload path passed the in-store containment check but the entry at that path is not a real directory — it is a regular file or a symbolic link (lstatSync reports isSymbolicLink or !isDirectory). flipCurrentAtomic creates a directory symlink, so the target must be a real directory; a file or symlink payload could break activation or be a vector for confusion.","triggerScenarios":"Called flipCurrentAtomic(payloadPath, paths) where payloadPath is inside installsRoot but fs.lstatSync(payloadPath) shows it is a file or a symlink rather than a real directory.","commonSituations":"1) The install payload extraction failed, leaving a file (e.g. a tarball) instead of an unpacked directory. 2) A symlink was placed at the payload path. 3) The payload directory was replaced with a file after install. 4) Wrong identifier passed such that it resolves to a file artifact.","solutions":["Inspect the payload path: 'ls -la <payloadPath>' and confirm it is a directory containing the unpacked package (node_modules/paperclipai/dist).","If it is a file or symlink, remove it and reinstall the payload so it is unpacked as a real directory.","If a symlink was intentionally placed, remove it and let the installer create a real payload directory.","Verify the identifier passed to payloadPathFor matches an actually-unpacked payload, not a downloaded archive."],"exampleFix":"$ ls -la ~/.paperclip/cli/installs/npm/1.2.3\n-rw-r--r--  1.2.3  (regular file, e.g. a tarball)\n$ rm ~/.paperclip/cli/installs/npm/1.2.3\n$ paperclipai install   # unpacks payload as real directory","handlingStrategy":"validation","validationCode":"import fs from \"node:fs\";\n\nfunction isRealPayloadDirectory(payloadPath: string): boolean {\n  try {\n    const st = fs.lstatSync(payloadPath);\n    return st.isDirectory() && !st.isSymbolicLink();\n  } catch { return false; }\n}\n\n// Before flipCurrentAtomic:\nif (!isRealPayloadDirectory(payload)) throw new Error(\"payload is not a real directory\");","typeGuard":"import fs from \"node:fs\";\n\nfunction isRealDirectory(p: string): boolean {\n  const st = fs.lstatSync(p);\n  return st.isDirectory() && !st.isSymbolicLink();\n}","tryCatchPattern":"try {\n  flipCurrentAtomic(payloadPath, paths);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"Refusing to activate non-directory payload\")) {\n    // payload extraction failed; reinstall the payload\n    throw err;\n  }\n  throw err;\n}","preventionTips":["Confirm the payload directory exists and is unpacked before activation.","Verify install extraction completed (exit code) before calling flipCurrentAtomic.","Never symlink the payload path; let the installer create a real directory.","Validate the payload contains node_modules/paperclipai/dist before activating."],"tags":["install-store","security","filesystem","payload","cli"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}