{"record":{"id":"cb4d8c689dbec028","repo":"paperclipai/paperclip","slug":"acpx-package-directory-changed-during-snapshot","errorCode":null,"errorMessage":"ACPX package directory changed during snapshot","messagePattern":"ACPX package directory changed during snapshot","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts","lineNumber":116,"sourceCode":"    if (++filesCopied > 30_000)\n      throw new Error(\"ACPX package snapshot exceeds its file bound\");\n    const before = await lstat(source, { bigint: true });\n    if (before.isSymbolicLink()) {\n      const canonical = await realpath(source);\n      const mapped = mapPath(canonical);\n      // Package-manager links to unqualified packages do not grant import authority.\n      if (mapped) await symlink(mapped, target);\n      return;\n    }\n    if (!within(root, await realpath(source)))\n      throw new Error(\"ACPX snapshot escaped its package\");\n    if (before.isDirectory()) {\n      await mkdir(target, { mode: 0o700 });\n      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 }))","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts#L98-L134","documentation":"After recursively copying a package directory, copy() re-stats the source and compares it (bigint identity: mode, size, mtime, etc.) with the stat taken before the copy. If the directory changed during the copy, the snapshot is inconsistent and the library throws. It is a TOCTOU integrity check guaranteeing the snapshot is a point-in-time consistent image.","triggerScenarios":"Any mutation (file added/removed/renamed, or metadata change) inside a source package directory between the initial lstat and the post-copy lstat while createAcpxPrivateSnapshot walks it.","commonSituations":"Running `npm/pnpm install`, a bundler watcher, a test runner emitting artifacts, or an editor/linter writing cache files inside the package tree while a session snapshot is taken.","solutions":["Quiesce all writers (installs, watchers, generators) before creating the snapshot and re-run it","Exclude volatile directories (caches, .cache, tmp output) from the package root","Re-run the snapshot after the tree stabilizes — the check is intentionally conservative","On CI, snapshot from a clean checkout rather than a live workspace"],"exampleFix":"// before: watcher active during snapshot\nconst watcher = startWatcher(pkgRoot);\nawait snapshot([pkgRoot]);\n// after\nwatcher.stop();\nawait snapshot([pkgRoot]);","handlingStrategy":"retry","validationCode":"const before = await fs.stat(dir);\nawait new Promise(r => setTimeout(r, 250));\nconst after = await fs.stat(dir);\nif (before.mtimeMs !== after.mtimeMs) throw new Error('directory is actively changing');","typeGuard":null,"tryCatchPattern":"try {\n  await createAcpxPrivateSnapshot({ roots });\n} catch (e) {\n  if (e.message.includes('directory changed during snapshot')) {\n    await waitForWritersToFinish(roots);\n    await createAcpxPrivateSnapshot({ roots });\n  } else throw e;\n}","preventionTips":["Stop watchers, generators, and installs before snapshotting","Keep caches and temp output out of package roots","Snapshot from clean checkouts or frozen images on CI"],"tags":["filesystem","race-condition","toctou","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"}