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

  1. Regenerate the eval request JSON so request.runnerd.sha256 is the digest of the current binary at request.runnerd.path
  2. Recompute the digest and update the field: sha256sum <runnerd-path>, keeping (or dropping) the 'sha256:' prefix — the code strips it either way
  3. Verify request.runnerd.path points at the exact runnerd binary you intend to test (resolve() is applied relative to cwd)
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-02). Data as JSON: /api/errors/6297fb45c67f4286. Report an issue: GitHub.