paperclipai/paperclip · error · Error

Registered base project workspace Paperclip config at ${conf

Error message

Registered base project workspace Paperclip config at ${configPath} cannot be inspected (${errorCode(error)} on its .paperclip symlink target).

What it means

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.

Source

Thrown at packages/shared/src/worktree-seed-source.ts:89

 * This tests directory entries and does not follow them. A dangling or aliased symlink,
 * at the config itself or at the `.paperclip` directory holding it, still counts as a
 * declared config, so the resolver rejects the malformed source instead of falling back
 * to another one.
 */
export function baseWorkspaceDeclaresInstanceConfig(baseWorkspaceCwd: string): boolean {
  const configDir = path.join(baseWorkspaceCwd, ".paperclip");
  const configPath = path.join(configDir, "config.json");
  if (inspectDeclaredEntry(configPath, configPath)) return true;

  // The probe above resolves `.paperclip` before it reaches the config, so a broken link
  // there also reports ENOENT. Only an absent or traversable `.paperclip` lets the caller
  // name another source; a link that hides whatever it points at is malformed, not empty.
  const configDirEntry = inspectDeclaredEntry(configDir, configPath, " on its .paperclip entry");
  if (configDirEntry?.isSymbolicLink()) {
    try {
      statSync(configDir);
    } catch (error) {
      throw new Error(
        `Registered base project workspace Paperclip config at ${configPath} cannot be inspected (${errorCode(error)} on its .paperclip symlink target).`,
      );
    }
  }
  return false;
}

function canonicalRegularFile(filePath: string, label: string): string {
  const resolved = path.resolve(filePath);
  let canonical: string;
  try {
    canonical = realpathSync(resolved);
  } catch {
    throw new Error(`${label} does not exist at ${resolved}.`);
  }
  if (canonical !== resolved || lstatSync(resolved).isSymbolicLink()) {
    throw new Error(`${label} must be a canonical path and cannot use a symlink alias.`);
  }

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. Inspect the link: `ls -la <base>/.paperclip` and `readlink <base>/.paperclip` to see the dead target.
  2. Restore the target at the recorded path, or remount the volume it lives on.
  3. 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.
  4. 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.

Example fix

# before: .paperclip -> /mnt/shared/paperclip-config (target missing)
rm /path/to/base/.paperclip
mkdir /path/to/base/.paperclip
cp /restored/config.json /path/to/base/.paperclip/config.json

# after: ls -la /path/to/base/.paperclip -> real directory holding config.json
Defensive patterns

Strategy: validation

Validate before calling

import { lstatSync, statSync } from "node:fs";
import path from "node:path";

function paperclipEntryIsSound(baseWorkspaceCwd: string): boolean {
  const dir = path.join(baseWorkspaceCwd, ".paperclip");
  const st = lstatSync(dir, { throwIfNoEntry: false });
  if (!st) return true; // absent is fine
  if (!st.isSymbolicLink()) return true;
  try { statSync(dir); return true; } catch { return false; } // dangling link
}

Type guard

const isDanglingPaperclipLink = (base: string): boolean => {
  try {
    const st = lstatSync(path.join(base, ".paperclip"));
    return st.isSymbolicLink() && !existsSync(path.join(base, ".paperclip"));
  } catch { return false; }
};

Try / catch

try {
  resolveRegisteredWorktreeSeedSource(input);
} catch (e) {
  if (e instanceof Error && e.message.includes("symlink target")) {
    // repair the .paperclip entry, then retry once
  } else throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-21). Data as JSON: /api/errors/0843f3f26d771df7. Report an issue: GitHub.