{"record":{"id":"330d5d037fdd821f","repo":"paperclipai/paperclip","slug":"acpx-module-changed-before-snapshot","errorCode":null,"errorMessage":"ACPX module changed before snapshot","messagePattern":"ACPX module changed before snapshot","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts","lineNumber":130,"sourceCode":"      directories.push(target);\n      for (const entry of await readdir(source))\n        await copy(join(source, entry), join(target, entry), root);\n      if (!same(before, await lstat(source, { bigint: true })))\n        throw new Error(\"ACPX package directory changed during snapshot\");\n      return;\n    }\n    if (!before.isFile() || before.size > 16n * 1024n * 1024n)\n      throw new Error(\"ACPX module must be a bounded regular file\");\n    bytesCopied += Number(before.size);\n    if (bytesCopied > MAX_PACKAGE_SNAPSHOT_BYTES)\n      throw new Error(\"ACPX package snapshot exceeds its byte bound\");\n    const handle = await open(\n      source,\n      constants.O_RDONLY | constants.O_NOFOLLOW,\n    );\n    try {\n      if (!same(before, await handle.stat({ bigint: true })))\n        throw new Error(\"ACPX module changed before snapshot\");\n      const bytes = await readSnapshotBytes(handle, Number(before.size));\n      if (\n        !same(before, await handle.stat({ bigint: true })) ||\n        !same(before, await lstat(source, { bigint: true }))\n      ) {\n        throw new Error(\"ACPX module changed during snapshot\");\n      }\n      await writeFile(target, bytes, { flag: \"wx\", mode: 0o400 });\n      digests[target] = digest(bytes);\n    } finally {\n      await handle.close();\n    }\n  };\n  try {\n    for (let index = 0; index < sourceRoots.length; index++) {\n      await copy(sourceRoots[index]!, roots[index]!, sourceRoots[index]!);\n      if (\n        !same(","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts#L112-L148","documentation":"Immediately after opening a module file with O_RDONLY|O_NOFOLLOW, copy() compares the open handle's stat against the pre-open lstat. If they differ — the file was swapped, resized, or rewritten between the initial lstat and open — it throws 'ACPX module changed before snapshot'. This closes the classic TOCTOU window before any bytes are read.","triggerScenarios":"A concurrent rename/replace (e.g. atomic install via rename), truncate, or chmod on a package file between the lstat that sized it and the open() call inside createAcpxPrivateSnapshot.","commonSituations":"Package manager performing an in-place upgrade during snapshot; build tooling rewriting output files; container image layer being updated while the runner snapshots.","solutions":["Ensure no installs/builds run concurrently with snapshot creation, then retry","Snapshot from an immutable copy (clean checkout, frozen container layer)","Exclude directories subject to atomic rename-replace from the admitted roots","Re-run after the mutating process exits — the error indicates a race, not corruption"],"exampleFix":"// before: snapshot during deploy\nawait deploy(); // rewrites package files\nawait snapshot(roots);\n// after\nawait deploy();\nawait waitForIdle(roots);\nawait snapshot(roots);","handlingStrategy":"retry","validationCode":"const st1 = await fs.stat(file);\nconst st2 = await fs.stat(file);\nif (st1.ino !== st2.ino || st1.mtimeMs !== st2.mtimeMs || st1.size !== st2.size) {\n  throw new Error('file is being rewritten; defer snapshot');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await createAcpxPrivateSnapshot({ roots });\n} catch (e) {\n  if (e.message.includes('changed before snapshot')) {\n    await new Promise(r => setTimeout(r, 1000));\n    await createAcpxPrivateSnapshot({ roots });\n  } else throw e;\n}","preventionTips":["Complete installs/builds before starting sessions","Avoid snapshotting directories subject to atomic rename-replace during deploys","Use immutable inputs (clean checkout, frozen layer) as snapshot sources"],"tags":["filesystem","race-condition","tocus","snapshot"],"backgroundTag":"file-changed-during-read","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"}