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
- 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.
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
- 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.
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
- ${label} does not exist at ${resolved}.
- ${label} must be a canonical path and cannot use a symlink a
- ${label} is not a regular file at ${canonical}.
- Registered source Paperclip config escapes the base project
- Registered ${label} Paperclip config is missing its adjacent
AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-21).
Data as JSON: /api/errors/0843f3f26d771df7.
Report an issue: GitHub.