paperclipai/paperclip · error · Error

${label} must be a canonical path and cannot use a symlink a

Error message

${label} must be a canonical path and cannot use a symlink alias.

What it means

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.

Source

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

    } 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.`);
  }
  if (!lstatSync(canonical).isFile()) {
    throw new Error(`${label} is not a regular file at ${canonical}.`);
  }
  return canonical;
}

/** Resolve the authoritative source and target identities without consulting diagnostics. */
export function resolveRegisteredWorktreeSeedSource(
  input: RegisteredWorktreeSeedSourceInput,
): CanonicalWorktreeSeedSource {
  const registeredCwd = input.registeredBaseWorkspaceCwd?.trim();
  const explicitSource = input.explicitSourceConfigPath?.trim();
  if (!registeredCwd && !explicitSource) {
    throw new Error(
      "Worktree seed source is not registered. Managed boot requires a project workspace; manual boot requires --from-config.",
    );
  }

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. Use the fully resolved path: pass `$(realpath <config>)` output instead of the alias.
  2. Remove the symlink and pass the real file path directly.
  3. Re-register the base workspace using its canonical (realpath) cwd so derived paths are canonical too.
  4. On macOS, beware /tmp → /private/tmp; anchor configs under a non-linked directory.

Example fix

# before
paperclip worktree seed --from-config /tmp/pc/config.json   # /tmp is a symlink on macOS

# after
paperclip worktree seed --from-config "$(realpath /tmp/pc/config.json)"
Defensive patterns

Strategy: validation

Validate before calling

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

function isCanonicalRegularFile(p: string): boolean {
  const resolved = path.resolve(p);
  try {
    if (realpathSync(resolved) !== resolved) return false;
    if (lstatSync(resolved).isSymbolicLink()) return false;
    return lstatSync(resolved).isFile();
  } catch { return false; }
}

Type guard

const isCanonicalPath = (p: string): boolean => {
  try { return realpathSync(p) === path.resolve(p); } catch { return false; }
};

Try / catch

try {
  resolveRegisteredWorktreeSeedSource(input);
} catch (e) {
  if (e instanceof Error && e.message.includes("cannot use a symlink alias")) {
    input.explicitSourceConfigPath = realpathSync(input.explicitSourceConfigPath!); // normalize then retry
  } else throw e;
}

Prevention

When it happens

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

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

Related errors


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