{"record":{"id":"c4ad88e5f502fa18","repo":"paperclipai/paperclip","slug":"registered-base-project-workspace-must-be-canonica","errorCode":null,"errorMessage":"Registered base project workspace must be canonical and cannot use a symlink alias.","messagePattern":"Registered base project workspace must be canonical and cannot use a symlink alias\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/src/worktree-seed-source.ts","lineNumber":136,"sourceCode":"  const registeredCwd = input.registeredBaseWorkspaceCwd?.trim();\n  const explicitSource = input.explicitSourceConfigPath?.trim();\n  if (!registeredCwd && !explicitSource) {\n    throw new Error(\n      \"Worktree seed source is not registered. Managed boot requires a project workspace; manual boot requires --from-config.\",\n    );\n  }\n\n  let canonicalBaseCwd: string | null = null;\n  let registeredConfigPath: string | null = null;\n  if (registeredCwd) {\n    const resolvedRegisteredCwd = path.resolve(registeredCwd);\n    try {\n      canonicalBaseCwd = realpathSync(resolvedRegisteredCwd);\n    } catch {\n      throw new Error(`Registered base project workspace does not exist at ${resolvedRegisteredCwd}.`);\n    }\n    if (canonicalBaseCwd !== resolvedRegisteredCwd) {\n      throw new Error(\"Registered base project workspace must be canonical and cannot use a symlink alias.\");\n    }\n    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\");","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/packages/shared/src/worktree-seed-source.ts#L118-L154","documentation":"The registered base workspace cwd exists, but realpathSync(resolved) differs from the resolved input, meaning the cwd reaches through a symlink alias. Downstream identity checks compare canonical strings, so the registration must already be canonical; an alias would let the same physical workspace be treated as two different ones.","triggerScenarios":"Registering a workspace via a symlinked path (e.g. /tmp on macOS resolving to /private/tmp, a linked home dir, or a shortcut link to the real checkout) and then booting worktree seed resolution from it.","commonSituations":"macOS /tmp and /var aliases; `ln -s ~/real/project ~/shortcut` used for convenience; HOME itself a symlink; CI checkouts under linked directories; Nix-style symlink trees.","solutions":["Re-register the workspace using its canonical path: `paperclip register --cwd \"$(realpath <dir>)\"`.","Remove the convenience symlink and operate on the real path.","Check `realpath <cwd>` vs the stored registration and make them byte-identical."],"exampleFix":"# before (macOS)\npaperclip register --cwd /tmp/scratch/myproj   # /tmp -> /private/tmp\n\n# after\npaperclip register --cwd \"$(realpath /tmp/scratch/myproj)\"","handlingStrategy":"validation","validationCode":"import { realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\nconst cwd = input.registeredBaseWorkspaceCwd!.trim();\nconst resolved = path.resolve(cwd);\nif (realpathSync(resolved) !== resolved) {\n  input = { ...input, registeredBaseWorkspaceCwd: realpathSync(resolved) }; // normalize before calling\n}","typeGuard":"const isCanonicalDir = (p: string): boolean => {\n  try { return realpathSync(p) === path.resolve(p); } catch { return false; }\n};","tryCatchPattern":"try {\n  resolveRegisteredWorktreeSeedSource(input);\n} catch (e) {\n  if (e instanceof Error && /must be canonical/.test(e.message)) {\n    input.registeredBaseWorkspaceCwd = realpathSync(input.registeredBaseWorkspaceCwd!);\n    return resolveRegisteredWorktreeSeedSource(input); // one retry with canonical path\n  }\n  throw e;\n}","preventionTips":["Store `realpath` output at registration time, never the raw user-supplied path.","Refuse convenience symlinks for workspace roots in team docs.","Log the canonical cwd at boot so alias drift is visible in logs."],"tags":["symlink","path-canonicalization","registration","worktree-seed"],"backgroundTag":"non-canonical-symlink-path","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}