{"record":{"id":"6297fb45c67f4286","repo":"paperclipai/paperclip","slug":"runnerd-digest-mismatch-expected-request-runner","errorCode":null,"errorMessage":"runnerd digest mismatch: expected ${request.runnerd.sha256}, got sha256:${actualDigest}","messagePattern":"runnerd digest mismatch: expected (.+?), got sha256:(.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/paperclip-runner/src/cli/eval-session.ts","lineNumber":283,"sourceCode":"  await session.shutdown(reason);\n}\n\nexport async function runEvalSessionCli(\n  args: string[],\n  options: {\n    serviceFactory?: (\n      runnerBinary: string,\n    ) => CapabilityLiveSessionService;\n  } = {},\n): Promise<number> {\n  const cli = parseEvalSessionCliArgs(args);\n  const request = parseEvalSessionRequest(\n    JSON.parse(await readFile(cli.requestPath, \"utf8\")),\n  );\n  const runnerdPath = resolve(request.runnerd.path);\n  const actualDigest = await sha256(runnerdPath);\n  if (actualDigest !== request.runnerd.sha256.replace(/^sha256:/, \"\")) {\n    throw new Error(\n      `runnerd digest mismatch: expected ${request.runnerd.sha256}, got sha256:${actualDigest}`,\n    );\n  }\n\n  const startedAt = new Date().toISOString();\n  const startedAtMs = Date.now();\n  const requestedProvider = request.provider ?? \"codex\";\n  const requestedDriver = request.driver ??\n    expectedEvalSessionDriver(requestedProvider);\n  const requestedProviderVersion = evalSessionProviderVersion(request);\n  const runtimeContext = await prepareEvalRuntimeContext(\n    resolve(request.session.workingDirectory ?? process.cwd()),\n  );\n  const service = options.serviceFactory?.(runnerdPath) ??\n    new CapabilityLiveSessionService({\n      transportOptions: {\n        ...evalProviderTransportOptions(requestedProvider, request.limits.turnTimeoutMs),\n        runnerBinary: runnerdPath,","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/cli/eval-session.ts#L265-L301","documentation":"runEvalSessionCli verifies the integrity of the runnerd binary before spawning it: it hashes the resolved runnerd.path file with SHA-256 and compares against request.runnerd.sha256 (stripping an optional 'sha256:' prefix). A mismatch throws this error, refusing to execute a binary that differs from the one the request was built against.","triggerScenarios":"The file at request.runnerd.path was rebuilt, recompiled, or replaced after the request's sha256 was computed; the path in the request points to a different runnerd build than intended; the digest string in the request was hand-edited or generated from a different file.","commonSituations":"Recompiling runnerd (e.g. after 'pnpm build') without regenerating the eval request; copying a request JSON between machines with different builds; pointing request.runnerd.path at a stale or alternate binary location; platform-specific rebuild changing the bytes.","solutions":["Regenerate the eval request JSON so request.runnerd.sha256 is the digest of the current binary at request.runnerd.path","Recompute the digest and update the field: sha256sum <runnerd-path>, keeping (or dropping) the 'sha256:' prefix — the code strips it either way","Verify request.runnerd.path points at the exact runnerd binary you intend to test (resolve() is applied relative to cwd)","If the binary was intentionally rebuilt, rebuild the request via the tooling that originally produced it rather than editing the digest by hand"],"exampleFix":"// before (request JSON, stale digest)\n\"runnerd\": { \"path\": \"./dist/runnerd\", \"sha256\": \"sha256:aaa...\" }\n// after\nsha256sum ./dist/runnerd  # bbb...\n\"runnerd\": { \"path\": \"./dist/runnerd\", \"sha256\": \"sha256:bbb...\" }","handlingStrategy":"validation","validationCode":"import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\n\nasync function verifyRunnerdDigest(request: { runnerd: { path: string; sha256: string } }): Promise<void> {\n  const actual = createHash(\"sha256\")\n    .update(await readFile(request.runnerd.path))\n    .digest(\"hex\");\n  const expected = request.runnerd.sha256.replace(/^sha256:/, \"\");\n  if (actual !== expected) {\n    throw new Error(\n      `stale runnerd digest for ${request.runnerd.path}: expected ${expected}, got ${actual} — regenerate the request JSON`,\n    );\n  }\n}","typeGuard":"function isSha256Hex(value: string): boolean {\n  return /^sha256:[0-9a-f]{64}$/.test(value) || /^[0-9a-f]{64}$/.test(value);\n}","tryCatchPattern":"try {\n  await runEvalSessionCli(args);\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith(\"runnerd digest mismatch\")) {\n    console.error(`${error.message}\\nThe runnerd binary changed since the request was generated. Rebuild the eval request JSON (recompute request.runnerd.sha256).`);\n    process.exitCode = 1;\n    return;\n  }\n  throw error;\n}","preventionTips":["Regenerate the eval request JSON as part of the same build step that produces runnerd — never check in digests by hand","Compute digests with sha256sum (or node:crypto) rather than copying them between environments","Pin the exact runnerd binary path in generated requests and rebuild requests whenever the binary changes","If requests are produced by a script, have it run sha256 immediately before writing the JSON so digests cannot go stale"],"tags":["integrity","checksum","supply-chain","runnerd"],"backgroundTag":"checksum-mismatch","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-02T18:44:00.616Z","contentChangedAt":"2026-09-02T18:44:00.616Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}