{"record":{"id":"0843f3f26d771df7","repo":"paperclipai/paperclip","slug":"registered-base-project-workspace-paperclip-config-0843f3","errorCode":null,"errorMessage":"Registered base project workspace Paperclip config at ${configPath} cannot be inspected (${errorCode(error)} on its .paperclip symlink target).","messagePattern":"Registered base project workspace Paperclip config at (.+?) cannot be inspected \\((.+?) on its \\.paperclip symlink target\\)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/src/worktree-seed-source.ts","lineNumber":89,"sourceCode":" * This tests directory entries and does not follow them. A dangling or aliased symlink,\n * at the config itself or at the `.paperclip` directory holding it, still counts as a\n * declared config, so the resolver rejects the malformed source instead of falling back\n * to another one.\n */\nexport function baseWorkspaceDeclaresInstanceConfig(baseWorkspaceCwd: string): boolean {\n  const configDir = path.join(baseWorkspaceCwd, \".paperclip\");\n  const configPath = path.join(configDir, \"config.json\");\n  if (inspectDeclaredEntry(configPath, configPath)) return true;\n\n  // The probe above resolves `.paperclip` before it reaches the config, so a broken link\n  // there also reports ENOENT. Only an absent or traversable `.paperclip` lets the caller\n  // name another source; a link that hides whatever it points at is malformed, not empty.\n  const configDirEntry = inspectDeclaredEntry(configDir, configPath, \" on its .paperclip entry\");\n  if (configDirEntry?.isSymbolicLink()) {\n    try {\n      statSync(configDir);\n    } 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  }","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/packages/shared/src/worktree-seed-source.ts#L71-L107","documentation":"The registered base project workspace's `.paperclip` entry is a symbolic link, and following that link (statSync) fails, so Paperclip cannot tell what it points at. Because a symlinked `.paperclip` still counts as a declared instance config, the resolver refuses to guess and aborts instead of silently falling back to another seed source. The wrapped code (usually ENOENT) names why the link target is unreachable.","triggerScenarios":"resolveRegisteredWorktreeSeedSource / resolveCanonicalWorktreeSeedSource (via worktree seed boot) on a base workspace where `.paperclip` is a symlink whose target was deleted, moved, or is unreadable (e.g. points into another checkout or an external drive that is not mounted).","commonSituations":"Users symlink `.paperclip` to a shared config location, then move or delete the target; macOS/Linux workspaces copied with `cp -a` preserving dangling links; NAS/external volume targets that are unmounted; migrating a workspace to a new path while leaving links behind.","solutions":["Inspect the link: `ls -la <base>/.paperclip` and `readlink <base>/.paperclip` to see the dead target.","Restore the target at the recorded path, or remount the volume it lives on.","Replace the symlink with a real directory (`rm <base>/.paperclip && mkdir <base>/.paperclip` plus a real config.json), or copy the target's config.json into a real `.paperclip/` directory.","If the base workspace intentionally has no instance config, delete the dangling `.paperclip` link entirely so the resolver can treat it as a plain checkout and accept an explicit --from-config source."],"exampleFix":"# before: .paperclip -> /mnt/shared/paperclip-config (target missing)\nrm /path/to/base/.paperclip\nmkdir /path/to/base/.paperclip\ncp /restored/config.json /path/to/base/.paperclip/config.json\n\n# after: ls -la /path/to/base/.paperclip -> real directory holding config.json","handlingStrategy":"validation","validationCode":"import { lstatSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\n\nfunction paperclipEntryIsSound(baseWorkspaceCwd: string): boolean {\n  const dir = path.join(baseWorkspaceCwd, \".paperclip\");\n  const st = lstatSync(dir, { throwIfNoEntry: false });\n  if (!st) return true; // absent is fine\n  if (!st.isSymbolicLink()) return true;\n  try { statSync(dir); return true; } catch { return false; } // dangling link\n}","typeGuard":"const isDanglingPaperclipLink = (base: string): boolean => {\n  try {\n    const st = lstatSync(path.join(base, \".paperclip\"));\n    return st.isSymbolicLink() && !existsSync(path.join(base, \".paperclip\"));\n  } catch { return false; }\n};","tryCatchPattern":"try {\n  resolveRegisteredWorktreeSeedSource(input);\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"symlink target\")) {\n    // repair the .paperclip entry, then retry once\n  } else throw e;\n}","preventionTips":["Never symlink `.paperclip`; keep it a real directory inside the workspace.","Run a workspace doctor check (lstat + stat on `.paperclip`) right after cloning or moving checkouts.","Exclude `.paperclip` from rsync/cp -a operations that preserve symlinks across machines."],"tags":["symlink","filesystem","worktree-seed","config"],"backgroundTag":"dangling-symlink","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}