paperclipai/paperclip · error · Error

Public report contains a broken or unsafe link in ${file}: $

Error message

Public report contains a broken or unsafe link in ${file}: ${href}

What it means

Thrown by validatePublicProtocolEvalReport when a link's href passed the safety check but its resolved target either falls outside the allowlisted public report paths (isPublicProtocolEvalPath fails) or does not exist as a file in the report directory. This guarantees every internal link in the published report resolves to a real, publishable file, preventing 404s and links smuggling paths outside the bundle.

Source

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

          payload.navigation?.previous?.href,
          payload.navigation?.next?.href,
        ].filter(Boolean)
      : [];
    for (const href of [...internalHtmlHrefs(content), ...navigation]) {
      if (typeof href !== "string" || /[?:\\]|^\/|^[a-z]+:/i.test(href))
        throw new Error(`Unsafe report navigation in ${file}`);
      const clean = href.split("#", 1)[0].split("?", 1)[0];
      const target = resolve(
        root,
        ...file.split("/").slice(0, -1),
        ...clean.split("/"),
      );
      const rel = relative(root, target).split(sep).join("/");
      if (
        !isPublicProtocolEvalPath(rel) ||
        !(await lstat(target).catch(() => null))?.isFile()
      ) {
        throw new Error(
          `Public report contains a broken or unsafe link in ${file}: ${href}`,
        );
      }
    }
  }
  if (viewer) {
    for (const file of viewer.files.keys())
      if (!files.includes(file))
        throw new Error(`Missing public viewer asset: ${file}`);
    if (files.some((file) => /^attempts\/[^/]+\.html$/.test(file)))
      throw new Error(
        "Chat Evalbook must not mix in legacy plain attempt pages",
      );
  }
  const campaign = await loadObject(join(root, "campaign.json"));
  if (
    campaign.schema !== "paperclip.runner-protocol-eval.campaign/v1" ||
    !SAFE_CAMPAIGN.test(String(campaign.campaignId ?? ""))

View on GitHub (pinned to 01ad858492)

Solutions

  1. Open the named file, find the href shown in the error, and fix or remove the broken link.
  2. Regenerate the report so the linked target file is actually produced.
  3. Check the target path matches one of the SAFE_REPORT_PATHS patterns (index/latest/inventory/real-server.html, tests/*.html, attempts/*, viewer/assets/*, campaign.json) and rename if needed.
  4. Verify file-name casing matches the href exactly before publishing.

Example fix

// before: link references ungenerated page
<a href="../tests/auth-refresh.html">Auth refresh</a>
// after: only link to generated targets, or generate the target
{#if tests.includes('auth-refresh')}<a href="../tests/auth-refresh.html">Auth refresh</a>{/if}
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from "node:fs/promises";
import { relative, resolve, sep } from "node:path";
export async function assertLinksResolve(root, file, hrefs) {
  for (const href of hrefs) {
    const clean = href.split("#", 1)[0].split("?", 1)[0];
    const target = resolve(root, ...file.split("/").slice(0, -1), ...clean.split("/"));
    const rel = relative(root, target).split(sep).join("/");
    if (!(await lstat(target).catch(() => null))?.isFile())
      throw new Error(`Broken link in ${file}: ${href}`);
  }
}

Try / catch

try {
  await validatePublicProtocolEvalReport(reportRoot, { viewerRoot });
} catch (err) {
  if (String(err.message).startsWith("Public report contains a broken or unsafe link")) {
    console.error("Fix or remove the broken href, or regenerate the missing target page.");
  }
  throw err;
}

Prevention

When it happens

Trigger: A report page href points to a file that was never generated (e.g. tests/missing.html), a file exists but violates SAFE_REPORT_PATHS (e.g. odd characters or disallowed extension), a relative path like ../../campaign.json resolves outside the report root, or the target file was deleted between generation and publication.

Common situations: Generator emits links conditionally on tests that failed to render; renaming an attempt directory invalidates cross-links from the index; a hand-edited page links to a file not included in the upload; case-sensitivity mismatch (Foo.html vs foo.html) on a case-insensitive dev machine.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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