paperclipai/paperclip · critical
runnerd digest mismatch: expected ${request.runnerd.sha256},
Error message
runnerd digest mismatch: expected ${request.runnerd.sha256}, got sha256:${actualDigest} What it means
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.
Source
Thrown at packages/paperclip-runner/src/cli/eval-session.ts:283
await session.shutdown(reason);
}
export async function runEvalSessionCli(
args: string[],
options: {
serviceFactory?: (
runnerBinary: string,
) => CapabilityLiveSessionService;
} = {},
): Promise<number> {
const cli = parseEvalSessionCliArgs(args);
const request = parseEvalSessionRequest(
JSON.parse(await readFile(cli.requestPath, "utf8")),
);
const runnerdPath = resolve(request.runnerd.path);
const actualDigest = await sha256(runnerdPath);
if (actualDigest !== request.runnerd.sha256.replace(/^sha256:/, "")) {
throw new Error(
`runnerd digest mismatch: expected ${request.runnerd.sha256}, got sha256:${actualDigest}`,
);
}
const startedAt = new Date().toISOString();
const startedAtMs = Date.now();
const requestedProvider = request.provider ?? "codex";
const requestedDriver = request.driver ??
expectedEvalSessionDriver(requestedProvider);
const requestedProviderVersion = evalSessionProviderVersion(request);
const runtimeContext = await prepareEvalRuntimeContext(
resolve(request.session.workingDirectory ?? process.cwd()),
);
const service = options.serviceFactory?.(runnerdPath) ??
new CapabilityLiveSessionService({
transportOptions: {
...evalProviderTransportOptions(requestedProvider, request.limits.turnTimeoutMs),
runnerBinary: runnerdPath,View on GitHub (pinned to 01ad858492)
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
Example fix
// before (request JSON, stale digest)
"runnerd": { "path": "./dist/runnerd", "sha256": "sha256:aaa..." }
// after
sha256sum ./dist/runnerd # bbb...
"runnerd": { "path": "./dist/runnerd", "sha256": "sha256:bbb..." } Defensive patterns
Strategy: validation
Validate before calling
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
async function verifyRunnerdDigest(request: { runnerd: { path: string; sha256: string } }): Promise<void> {
const actual = createHash("sha256")
.update(await readFile(request.runnerd.path))
.digest("hex");
const expected = request.runnerd.sha256.replace(/^sha256:/, "");
if (actual !== expected) {
throw new Error(
`stale runnerd digest for ${request.runnerd.path}: expected ${expected}, got ${actual} — regenerate the request JSON`,
);
}
} Type guard
function isSha256Hex(value: string): boolean {
return /^sha256:[0-9a-f]{64}$/.test(value) || /^[0-9a-f]{64}$/.test(value);
} Try / catch
try {
await runEvalSessionCli(args);
} catch (error) {
if (error instanceof Error && error.message.startsWith("runnerd digest mismatch")) {
console.error(`${error.message}\nThe runnerd binary changed since the request was generated. Rebuild the eval request JSON (recompute request.runnerd.sha256).`);
process.exitCode = 1;
return;
}
throw error;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Materialized OpenCode executable digest mismatch
- Public viewer asset differs from trusted build: ${file}
- ACPX ${agent} runtime executable digest mismatch
- ACPX snapshot manifest digest mismatch
- ACPX private snapshot digest mismatch
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-02).
Data as JSON: /api/errors/6297fb45c67f4286.
Report an issue: GitHub.