{"record":{"id":"030632bb991a878c","repo":"paperclipai/paperclip","slug":"public-report-contains-credential-session-material","errorCode":null,"errorMessage":"Public report contains credential/session material: ${file}","messagePattern":"Public report contains credential/session material: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs","lineNumber":184,"sourceCode":"      );\n    }\n    if (file.startsWith(\"viewer/\")) {\n      const expected = viewer?.files.get(file);\n      if (!expected || !expected.equals(await readFile(absolute)))\n        throw new Error(\n          `Public viewer asset differs from trusted build: ${file}`,\n        );\n      continue;\n    }\n    const content = await readFile(absolute, \"utf8\");\n    const richAttempt = /^attempts\\/[^/]+\\/index\\.html$/.test(file);\n    const payload = richAttempt\n      ? validatePublicViewerPage(content, viewer.index)\n      : null;\n    if (!richAttempt) {\n      for (const pattern of CREDENTIAL_PATTERNS) {\n        if (pattern.test(content))\n          throw new Error(\n            `Public report contains credential/session material: ${file}`,\n          );\n      }\n      if (extname(file) !== \".html\") continue;\n      for (const pattern of ACTIVE_HTML_PATTERNS) {\n        if (pattern.test(content))\n          throw new Error(\n            `Public report contains active or remote HTML: ${file}`,\n          );\n      }\n    }\n    const navigation = payload\n      ? [\n          payload.navigation?.suiteHref,\n          payload.navigation?.previous?.href,\n          payload.navigation?.next?.href,\n        ].filter(Boolean)\n      : [];","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs#L166-L202","documentation":"This error is thrown by validatePublicProtocolEvalReport when a non-rich-attempt file in the public protocol eval report directory matches one of the CREDENTIAL_PATTERNS (AWS access keys like AKIA..., sk- API keys, Bearer tokens, or providerSessionId/sessionId JSON keys). The script is about to publish this directory to a public S3 URL, so any embedded credential or session material would leak secrets publicly. The check runs against every file in the report that is not a rich attempt page (attempts/<id>/index.html).","triggerScenarios":"Running publishProtocolEvalHistory (or the script directly) when a report file such as index.html, tests/*.html, attempts/*.html, or campaign.json contains text matching AKIA[0-9A-Z]{16}, sk-[A-Za-z0-9_-]{20,}, `Bearer <token>` (16+ chars), or a JSON key \"providerSessionId\"/\"sessionId\".","commonSituations":"Agents under evaluation echo environment variables or API keys into their transcript output; a captured Bearer token from an HTTP request log is pasted into an attempt page; a sessionId field from a provider response is serialized into campaign.json or a test result page; CI re-publishes an old report directory generated before secret redaction was added.","solutions":["Find the offending file (named in the error) and remove or redact the matching credential/session string, then regenerate the report.","Add redaction at report-generation time (e.g. scrub secrets from agent transcripts before writing HTML/JSON).","If a session ID is legitimate non-secret metadata, rename the JSON key so it does not match the sessionId/providerSessionId pattern.","Regenerate the report from source rather than editing the published copy, then re-run the publish script."],"exampleFix":"// before: report HTML embeds raw transcript\n<pre>$ export OPENAI_API_KEY=sk-abc123def456ghijklmno</pre>\n// after: redact secrets before rendering\nconst redacted = transcript.replace(/sk-[A-Za-z0-9_-]{20,}/g, \"sk-[REDACTED]\");","handlingStrategy":"validation","validationCode":"import { readFile } from \"node:fs/promises\";\nconst CREDENTIAL_PATTERNS = [\n  /\\bAKIA[0-9A-Z]{16}\\b/u,\n  /\\bsk-[A-Za-z0-9_-]{20,}\\b/u,\n  /\\bBearer\\s+[A-Za-z0-9._~-]{16,}\\b/iu,\n  /[\"'](?:providerSessionId|sessionId)[\"']\\s*:/u,\n];\nexport async function assertNoSecrets(file) {\n  const content = await readFile(file, \"utf8\");\n  const hit = CREDENTIAL_PATTERNS.findIndex((p) => p.test(content));\n  if (hit !== -1) throw new Error(`Secret-like content in ${file} (pattern #${hit})`);\n}","typeGuard":"const containsSecret = (content) => CREDENTIAL_PATTERNS.some((p) => p.test(content));","tryCatchPattern":"try {\n  await publishProtocolEvalHistory({ reportRoot, destination, viewerRoot });\n} catch (err) {\n  if (String(err.message).startsWith(\"Public report contains credential/session material\")) {\n    console.error(\"Redact secrets in the named file and regenerate the report before publishing.\");\n  }\n  throw err;\n}","preventionTips":["Redact secrets in agent transcripts at capture time, before any HTML rendering.","Run validatePublicProtocolEvalReport locally in CI before the publish job.","Keep provider session IDs out of serialized report JSON, or rename those keys.","Never publish raw agent environment dumps; scrub env vars from captured output."],"tags":["security","secrets-leak","publish-validation"],"backgroundTag":"missing-credentials","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}