paperclipai/paperclip · error · Error
Refusing public report symlink
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.
Solutions
- Remove symlinks from the report directory or replace them with real copies before publishing.
- Build the report with real files only (e.g. cp -L to dereference when copying).
- 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
- Build report artifacts with dereferenced copies (cp -L / rsync without --links).
- Scan the report directory for symlinks before publishing.
- Avoid creating 'latest' symlink shortcuts inside published folders.
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
- A trusted viewer build is required for public chat reports
- ACPX runtime executable changed while it was verified
- ACPX runtime executable could not be opened as a no-follow…
- ACPX runtime executable must be a bounded executable file
- ACPX runtime executable must be a real regular file
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/b93a8e73f37e85d7.
Report an issue: GitHub.
Appendix: 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)