paperclipai/paperclip · error · Error

Registered source Paperclip config escapes the base project

Error message

Registered source Paperclip config escapes the base project workspace or uses a symlink alias.

What it means

The base workspace declares its own config, so the expected source is `<base>/.paperclip/config.json`, but the canonical path of that selection differs from the registered joined path. That means some component (typically a symlinked `.paperclip` directory) resolves outside the base workspace, so the 'registered' config is actually stored elsewhere — rejected as an escape/alias.

Source

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

    if (!lstatSync(canonicalBaseCwd).isDirectory()) {
      throw new Error(`Registered base project workspace is not a directory at ${canonicalBaseCwd}.`);
    }
    // A base workspace that is a plain checkout carries no instance config of its own.
    // The caller's explicit source supplies it, and stays subject to every check below.
    registeredConfigPath = baseWorkspaceDeclaresInstanceConfig(canonicalBaseCwd)
      ? path.join(canonicalBaseCwd, ".paperclip", "config.json")
      : null;
  }

  const selectedPath = registeredConfigPath ?? explicitSource;
  if (!selectedPath) {
    throw new Error(
      "Registered base project workspace has no Paperclip config of its own and no explicit source was provided.",
    );
  }
  const canonicalSourceConfigPath = canonicalRegularFile(selectedPath, "Registered source Paperclip config");
  if (registeredConfigPath && canonicalSourceConfigPath !== registeredConfigPath) {
    throw new Error("Registered source Paperclip config escapes the base project workspace or uses a symlink alias.");
  }

  if (explicitSource) {
    const canonicalExplicitSource = canonicalRegularFile(explicitSource, "Explicit source Paperclip config");
    if (canonicalExplicitSource !== canonicalSourceConfigPath) {
      throw new Error("Explicit source Paperclip config does not match the registered base project workspace.");
    }
  }

  const canonicalTargetConfigPath = canonicalRegularFile(
    input.targetConfigPath,
    "Target worktree Paperclip config",
  );
  if (canonicalSourceConfigPath === canonicalTargetConfigPath) {
    throw new Error("Source and target Paperclip configs are the same canonical file.");
  }

  const sourceInstanceId = readInstanceId(canonicalSourceConfigPath, "source");

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. Check for links along the path: `namei -l <base>/.paperclip/config.json`.
  2. Replace the symlinked `.paperclip` with a real directory containing a real config.json inside the workspace.
  3. If sharing one instance across worktrees is the goal, use the supported explicit --from-config flow instead of symlinking `.paperclip`.

Example fix

# before
ln -s ~/.paperclip-shared ~/code/myproj/.paperclip   # escapes base workspace

# after
rm ~/code/myproj/.paperclip
mkdir ~/code/myproj/.paperclip
cp ~/.paperclip-shared/config.json ~/code/myproj/.paperclip/config.json
Defensive patterns

Strategy: validation

Validate before calling

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

function configStaysInsideBase(base: string): boolean {
  const joined = path.join(base, ".paperclip", "config.json");
  try { return realpathSync(joined) === joined; } catch { return false; }
}
const st = lstatSync(path.join(base, ".paperclip"), { throwIfNoEntry: false });
if (st?.isSymbolicLink()) throw new Error(".paperclip must not be a symlink");

Type guard

const isContainedRealPath = (base: string, rel: string): boolean => {
  const joined = path.join(base, rel);
  try { return realpathSync(joined) === joined; } catch { return false; }
};

Try / catch

try {
  resolveRegisteredWorktreeSeedSource(input);
} catch (e) {
  if (e instanceof Error && /escapes the base project workspace/.test(e.message)) {
    // materialize .paperclip as a real dir with a copied config, then retry
  } else throw e;
}

Prevention

When it happens

Trigger: `.paperclip` being a symlink to a config directory in another location (shared across checkouts); a parent path component of the config being a link; canonicalRegularFile returning a realpath that differs from path.join(base, '.paperclip', 'config.json').

Common situations: Sharing one instance config among multiple clones via `ln -s` (a workflow the invariant explicitly forbids); dotfiles managers (stow, chezmoi) symlinking `.paperclip`; container layers linking config into the workspace.

Related errors


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