{"record":{"id":"85375cdf4c62551c","repo":"paperclipai/paperclip","slug":"refusing-to-replace-non-symlink-paths-currentpat","errorCode":null,"errorMessage":"Refusing to replace non-symlink ${paths.currentPath}.","messagePattern":"Refusing to replace non-symlink (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/install-store.ts","lineNumber":273,"sourceCode":"  }\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()) {\n      throw new Error(`Refusing to replace non-symlink ${paths.currentPath}.`);\n    }\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n  }\n\n  const temporaryLink = path.join(\n    paths.cliRoot,\n    `.current-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,\n  );\n  const relativeTarget = path.relative(paths.cliRoot, payloadPath);\n  try {\n    fs.symlinkSync(relativeTarget, temporaryLink, \"dir\");\n    hooks.beforeRename?.();\n    fs.renameSync(temporaryLink, paths.currentPath);\n  } finally {\n    fs.rmSync(temporaryLink, { force: true });\n  }\n}","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/install-store.ts#L255-L291","documentation":"Thrown by flipCurrentAtomic when paths.currentPath exists but is not a symbolic link. The atomic-swap strategy creates a temporary symlink and renames it over 'current'; this guard ensures 'current' was never replaced by a regular file or directory behind the installer's back, preventing data loss or clobbering of a non-symlink artifact.","triggerScenarios":"Calling flipCurrentAtomic after something has replaced paths.currentPath (the 'current' symlink under cliRoot) with a regular file or a real directory. This can happen if a user manually created a file/directory named 'current' or if a different install mechanism wrote into that path.","commonSituations":"Manual filesystem manipulation inside ~/.<paperclip-home>/cli/, a broken previous install that left a directory instead of a symlink, or a user copied a directory into 'current' expecting it to be the active install.","solutions":["Inspect paths.currentPath: run 'ls -la <cliRoot>/current' to see what type of filesystem object occupies it.","If it is a stale directory or file, remove it so the atomic swap can create a fresh symlink: 'rm -rf <cliRoot>/current'.","Re-run the install or activation command that triggered flipCurrentAtomic.","If 'current' should legitimately be a directory, review your install workflow—it must always be managed as a symlink by this code."],"exampleFix":"// before: current is a real directory\n// ls -la ~/.paperclip/cli/current -> drwxr-xr-x\n\n// after: remove it so flipCurrentAtomic can create the symlink\nfs.rmSync(paths.currentPath, { recursive: true, force: true });\n// now re-run activation","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\n\nfunction ensureCurrentIsSymlinkOrAbsent(currentPath: string): void {\n  try {\n    const stat = fs.lstatSync(currentPath);\n    if (!stat.isSymbolicLink()) {\n      fs.rmSync(currentPath, { recursive: true, force: true });\n    }\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n  }\n}\n\n// Call before flipCurrentAtomic:\nensureCurrentIsSymlinkOrAbsent(paths.currentPath);","typeGuard":null,"tryCatchPattern":"try {\n  flipCurrentAtomic(payloadPath, paths);\n} catch (error) {\n  if (error instanceof Error && error.message.includes('Refusing to replace non-symlink')) {\n    // 'current' was clobbered with a file/dir; decide whether to remove it\n    console.error('current path is not a symlink:', paths.currentPath);\n  }\n  throw error;\n}","preventionTips":["Never manually create files or directories at paths.currentPath (~/.<paperclip-home>/cli/current).","The 'current' path must always be a symlink managed exclusively by flipCurrentAtomic.","If cleaning up a broken install, remove the entire cliRoot rather than just 'current'."],"tags":["install-store","symlink","atomic-swap","cli"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}