{"record":{"id":"95bf30df47dd3dad","repo":"paperclipai/paperclip","slug":"label-must-be-a-canonical-path-and-cannot-use-a","errorCode":null,"errorMessage":"${label} must be a canonical path and cannot use a symlink alias.","messagePattern":"(.+?) must be a canonical path and cannot use a symlink alias\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/src/worktree-seed-source.ts","lineNumber":106,"sourceCode":"    } catch (error) {\n      throw new Error(\n        `Registered base project workspace Paperclip config at ${configPath} cannot be inspected (${errorCode(error)} on its .paperclip symlink target).`,\n      );\n    }\n  }\n  return false;\n}\n\nfunction canonicalRegularFile(filePath: string, label: string): string {\n  const resolved = path.resolve(filePath);\n  let canonical: string;\n  try {\n    canonical = realpathSync(resolved);\n  } catch {\n    throw new Error(`${label} does not exist at ${resolved}.`);\n  }\n  if (canonical !== resolved || lstatSync(resolved).isSymbolicLink()) {\n    throw new Error(`${label} must be a canonical path and cannot use a symlink alias.`);\n  }\n  if (!lstatSync(canonical).isFile()) {\n    throw new Error(`${label} is not a regular file at ${canonical}.`);\n  }\n  return canonical;\n}\n\n/** Resolve the authoritative source and target identities without consulting diagnostics. */\nexport function resolveRegisteredWorktreeSeedSource(\n  input: RegisteredWorktreeSeedSourceInput,\n): CanonicalWorktreeSeedSource {\n  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  }","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/packages/shared/src/worktree-seed-source.ts#L88-L124","documentation":"canonicalRegularFile() requires that `path.resolve(p)` equal `realpathSync(p)` and that the path itself not be a symlink. If realpath resolves through any symlink component, or lstat on the resolved path reports a link, the config is considered a non-canonical alias and rejected, because identity checks later compare raw path strings.","triggerScenarios":"Passing a config path that is itself a symlink (`ln -s real.json alias.json`, then --from-config alias.json); paths containing symlinked directories such as macOS `/tmp` vs `/private/tmp` or a symlinked home directory; workspace checked out under a linked path.","commonSituations":"macOS temp/home dir aliasing; users shortening long instance paths with symlinks; CI runners where HOME or the checkout root is a link; Nix/store-style symlinked prefixes.","solutions":["Use the fully resolved path: pass `$(realpath <config>)` output instead of the alias.","Remove the symlink and pass the real file path directly.","Re-register the base workspace using its canonical (realpath) cwd so derived paths are canonical too.","On macOS, beware /tmp → /private/tmp; anchor configs under a non-linked directory."],"exampleFix":"# before\npaperclip worktree seed --from-config /tmp/pc/config.json   # /tmp is a symlink on macOS\n\n# after\npaperclip worktree seed --from-config \"$(realpath /tmp/pc/config.json)\"","handlingStrategy":"validation","validationCode":"import { lstatSync, realpathSync } from \"node:fs\";\nimport path from \"node:path\";\n\nfunction isCanonicalRegularFile(p: string): boolean {\n  const resolved = path.resolve(p);\n  try {\n    if (realpathSync(resolved) !== resolved) return false;\n    if (lstatSync(resolved).isSymbolicLink()) return false;\n    return lstatSync(resolved).isFile();\n  } catch { return false; }\n}","typeGuard":"const isCanonicalPath = (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 && e.message.includes(\"cannot use a symlink alias\")) {\n    input.explicitSourceConfigPath = realpathSync(input.explicitSourceConfigPath!); // normalize then retry\n  } else throw e;\n}","preventionTips":["Canonicalize once at input boundaries: store `realpathSync(p)` results, pass those onward.","On macOS, avoid /tmp and other system aliases for config paths.","Add a lint/CI check that registration and manifest paths pass a realpath-equals-input assertion."],"tags":["symlink","path-canonicalization","filesystem","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"}