paperclipai/paperclip · error · Error

Refusing public report symlink ${absolute}

Error message

Refusing public report symlink ${absolute}

What it means

Thrown by relativeFiles in the publish script while walking the report directory: symbolic links are refused outright so the published public report cannot leak or point at files outside the report root. Symlinks (including directory symlinks) abort the publish.

Source

Thrown at packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs:124

  if (
    relativePath.includes("\\") ||
    relativePath.startsWith("/") ||
    relativePath
      .split("/")
      .some((segment) => !segment || segment === "." || segment === "..")
  ) {
    return false;
  }
  return SAFE_REPORT_PATHS.some((pattern) => pattern.test(relativePath));
}

async function relativeFiles(root, current = root) {
  const entries = await readdir(current, { withFileTypes: true });
  const files = [];
  for (const entry of entries) {
    const absolute = join(current, entry.name);
    if (entry.isSymbolicLink())
      throw new Error(`Refusing public report symlink ${absolute}`);
    if (entry.isDirectory())
      files.push(...(await relativeFiles(root, absolute)));
    else if (entry.isFile())
      files.push(relative(root, absolute).split(sep).join("/"));
    else throw new Error(`Refusing unusual public report path ${absolute}`);
  }
  return files.sort();
}

function internalHtmlHrefs(content) {
  return [...content.matchAll(/href\s*=\s*["']([^"']+)["']/giu)]
    .map((match) => match[1])
    .filter((href) => href && !href.startsWith("#"));
}

export async function validatePublicProtocolEvalReport(
  reportRoot,
  { viewerRoot } = {},

View on GitHub (pinned to 01ad858492)

Solutions

  1. Remove symlinks from the report directory or replace them with real copies before publishing.
  2. Build the report with real files only (e.g. cp -L to dereference when copying).
  3. Point the publish script at a clean report directory without symlinked entries.

Example fix

// before
ln -s ./runs/v3 report/latest
// after
cp -L ./runs/v3/summary.json report/latest-summary.json
Defensive patterns

Strategy: validation

Validate before calling

for (const p of walk(reportDir)) {
  const st = fs.lstatSync(p);
  if (st.isSymbolicLink()) throw new Error(`symlink in report dir: ${p}`);
}

Try / catch

try { await publish(reportDir); } catch (e) { if (e.message.startsWith('Refusing public report symlink')) { console.error('Replace symlinks in the report directory with real files (cp -L).'); process.exit(1); } throw e; }

Prevention

When it happens

Trigger: Running the publish against a report directory that contains a symlinked file or subdirectory anywhere in the tree.

Common situations: A build step creates convenience symlinks (e.g. 'latest' -> 'v3') inside the report folder; checking out fixtures containing symlinks on macOS/Linux; copying directories with cp -s or rsync --links.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/b93a8e73f37e85d7. Report an issue: GitHub.