{"record":{"id":"620cb0b313a09033","repo":"paperclipai/paperclip","slug":"registered-source-paperclip-config-escapes-the-bas","errorCode":null,"errorMessage":"Registered source Paperclip config escapes the base project workspace or uses a symlink alias.","messagePattern":"Registered source Paperclip config escapes the base project workspace or uses a symlink alias\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/src/worktree-seed-source.ts","lineNumber":156,"sourceCode":"    if (!lstatSync(canonicalBaseCwd).isDirectory()) {\n      throw new Error(`Registered base project workspace is not a directory at ${canonicalBaseCwd}.`);\n    }\n    // A base workspace that is a plain checkout carries no instance config of its own.\n    // The caller's explicit source supplies it, and stays subject to every check below.\n    registeredConfigPath = baseWorkspaceDeclaresInstanceConfig(canonicalBaseCwd)\n      ? path.join(canonicalBaseCwd, \".paperclip\", \"config.json\")\n      : null;\n  }\n\n  const selectedPath = registeredConfigPath ?? explicitSource;\n  if (!selectedPath) {\n    throw new Error(\n      \"Registered base project workspace has no Paperclip config of its own and no explicit source was provided.\",\n    );\n  }\n  const canonicalSourceConfigPath = canonicalRegularFile(selectedPath, \"Registered source Paperclip config\");\n  if (registeredConfigPath && canonicalSourceConfigPath !== registeredConfigPath) {\n    throw new Error(\"Registered source Paperclip config escapes the base project workspace or uses a symlink alias.\");\n  }\n\n  if (explicitSource) {\n    const canonicalExplicitSource = canonicalRegularFile(explicitSource, \"Explicit source Paperclip config\");\n    if (canonicalExplicitSource !== canonicalSourceConfigPath) {\n      throw new Error(\"Explicit source Paperclip config does not match the registered base project workspace.\");\n    }\n  }\n\n  const canonicalTargetConfigPath = canonicalRegularFile(\n    input.targetConfigPath,\n    \"Target worktree Paperclip config\",\n  );\n  if (canonicalSourceConfigPath === canonicalTargetConfigPath) {\n    throw new Error(\"Source and target Paperclip configs are the same canonical file.\");\n  }\n\n  const sourceInstanceId = readInstanceId(canonicalSourceConfigPath, \"source\");","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/packages/shared/src/worktree-seed-source.ts#L138-L174","documentation":"The base workspace declares its own config, so the expected source is `<base>/.paperclip/config.json`, but the canonical path of that selection differs from the registered joined path. That means some component (typically a symlinked `.paperclip` directory) resolves outside the base workspace, so the 'registered' config is actually stored elsewhere — rejected as an escape/alias.","triggerScenarios":"`.paperclip` being a symlink to a config directory in another location (shared across checkouts); a parent path component of the config being a link; canonicalRegularFile returning a realpath that differs from path.join(base, '.paperclip', 'config.json').","commonSituations":"Sharing one instance config among multiple clones via `ln -s` (a workflow the invariant explicitly forbids); dotfiles managers (stow, chezmoi) symlinking `.paperclip`; container layers linking config into the workspace.","solutions":["Check for links along the path: `namei -l <base>/.paperclip/config.json`.","Replace the symlinked `.paperclip` with a real directory containing a real config.json inside the workspace.","If sharing one instance across worktrees is the goal, use the supported explicit --from-config flow instead of symlinking `.paperclip`."],"exampleFix":"# before\nln -s ~/.paperclip-shared ~/code/myproj/.paperclip   # escapes base workspace\n\n# after\nrm ~/code/myproj/.paperclip\nmkdir ~/code/myproj/.paperclip\ncp ~/.paperclip-shared/config.json ~/code/myproj/.paperclip/config.json","handlingStrategy":"validation","validationCode":"import { lstatSync, realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\nfunction configStaysInsideBase(base: string): boolean {\n  const joined = path.join(base, \".paperclip\", \"config.json\");\n  try { return realpathSync(joined) === joined; } catch { return false; }\n}\nconst st = lstatSync(path.join(base, \".paperclip\"), { throwIfNoEntry: false });\nif (st?.isSymbolicLink()) throw new Error(\".paperclip must not be a symlink\");","typeGuard":"const isContainedRealPath = (base: string, rel: string): boolean => {\n  const joined = path.join(base, rel);\n  try { return realpathSync(joined) === joined; } catch { return false; }\n};","tryCatchPattern":"try {\n  resolveRegisteredWorktreeSeedSource(input);\n} catch (e) {\n  if (e instanceof Error && /escapes the base project workspace/.test(e.message)) {\n    // materialize .paperclip as a real dir with a copied config, then retry\n  } else throw e;\n}","preventionTips":["Treat `.paperclip` as instance-private state: real directory, never a link.","Ban dotfile-stow tools from managing `.paperclip` in repo dotfiles config.","Add a repo lint that fails when `.paperclip` is a symlink."],"tags":["symlink","worktree-seed","config","path-containment"],"backgroundTag":"symlink-path-escape","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}