paperclipai/paperclip · error · Error

ACPX executable changed during snapshot

Error message

ACPX executable changed during snapshot

What it means

After reading the executable's bytes, createAcpxPrivateSnapshot re-stats the source file and compares it to the stat taken before reading. If size/mtime/inode changed between the two stats, the executable was concurrently modified and the snapshot would be a torn, inconsistent copy, so it throws instead of writing a corrupt snapshot.

Source

Thrown at packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts:199

            if (error.code !== "EEXIST") throw error;
          },
        );
      }
    let executablePath: string | null = null;
    if (executable) {
      const before = await executable.stat({ bigint: true });
      if (!before.isFile() || before.size < 1n || before.size > BigInt(MAX_ACPX_RUNTIME_EXECUTABLE_BYTES)) {
        throw new Error("ACPX runtime executable must be a bounded executable file");
      }
      // The bigint bound above makes this conversion exact before allocation.
      const executableBytes = Number(before.size);
      bytesCopied += executableBytes;
      if (bytesCopied > MAX_SNAPSHOT_BYTES) {
        throw new Error("ACPX snapshot exceeds its byte bound");
      }
      const bytes = await readSnapshotBytes(executable, executableBytes);
      if (!same(before, await executable.stat({ bigint: true })))
        throw new Error("ACPX executable changed during snapshot");
      executablePath = join(directory, "runtime");
      await writeFile(executablePath, bytes, { flag: "wx", mode: 0o500 });
      digests[executablePath] = digest(bytes);
    }
    const manifest = Buffer.from(
      JSON.stringify({ roots, executable: executablePath, digests }),
    );
    const manifestPath = join(directory, "manifest.json");
    await writeFile(manifestPath, manifest, { flag: "wx", mode: 0o400 });
    for (const dir of new Set(directories)) await chmod(dir, 0o500);
    return {
      roots,
      executable: executablePath,
      digests,
      handoff: { path: manifestPath, digest: digest(manifest) },
      close,
    };
  } catch (error) {

View on GitHub (pinned to 01ad858492)

Solutions

  1. Re-run the snapshot once the concurrent process (package manager, updater, build) has finished; the check is TOCTOU protection and usually succeeds on retry.
  2. Identify what mutates the executable (lsof/fuser on the path) and stop it from running concurrently.
  3. Pin/lock the ACPX runtime version so installs do not replace the binary mid-session.
  4. Exclude the runtime directory from antivirus/indexer interference, or move the executable to a stable location referenced by config.

Example fix

// before
await driver.snapshot(); // raced with `pnpm install` replacing the binary
// after
await exec("pnpm install --frozen-lockfile"); // quiesce first
await driver.snapshot();
Defensive patterns

Strategy: retry

Try / catch

try {
  await driver.snapshot();
} catch (e) {
  if (e.message === "ACPX executable changed during snapshot") {
    await waitForQuiescence(); // ensure no install/update is running
    return retrySnapshot(3);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling snapshot()/privateSnapshot() while another process (package manager, updater, build script, antivirus quarantine/rewrite) replaces or rewrites the ACPX runtime executable between the initial stat and the post-read verification stat.

Common situations: Running pnpm/yarn install or a build that relinks binaries in the same session; an auto-updater running concurrently; copying the repo while a tool rewrites timestamps; docker/CI image layers being mutated mid-run.

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-10). Data as JSON: /api/errors/2a3627abd28d989f. Report an issue: GitHub.