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
- Open the named file, find the href shown in the error, and fix or remove the broken link.
- Regenerate the report so the linked target file is actually produced.
- 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.
- 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
- Generate links only for pages the generator actually produced (link list from the render manifest).
- Check href casing against real file names, especially across case-insensitive filesystems.
- Avoid hand-editing published report files; always regenerate from source.
- Run the validator as a pre-publish CI step to catch link rot early.
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
- Public report contains active or remote HTML: ${file}
- Unsafe report navigation in ${file}
- Public viewer page differs from the trusted shell
- Public report contains credential/session material: ${file}
- Missing public viewer asset: ${file}
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/c4639621d076ce2c.
Report an issue: GitHub.