{"record":{"id":"e38b085b5cd3a025","repo":"paperclipai/paperclip","slug":"acpx-package-directory-changed-while-snapshotting","errorCode":null,"errorMessage":"ACPX package directory changed while snapshotting","messagePattern":"ACPX package directory changed while snapshotting","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts","lineNumber":753,"sourceCode":"        if (!sameIdentity(current.identity, commandIdentity)) {\n          current.bytes.fill(0);\n          throw new Error(\n            \"ACPX provider executable identity changed after verification\",\n          );\n        }\n        const privateSnapshot = process.platform === \"darwin\"\n          ? await createAcpxPrivateSnapshot([commandDirectory, ...dependencyAncestors.map((root) => root.path)], currentRuntimeExecutable)\n          : null;\n        if (privateSnapshot) {\n          try {\n            // Bind copied trees to the identities retained by the verified lease.\n            const paths = [commandDirectory, ...dependencyAncestors.map((root) => root.path)];\n            const handles = [currentDirectory.handle, ...currentDependencyAncestors];\n            for (let index = 0; index < paths.length; index++) {\n              const lexical = await lstat(paths[index]!, { bigint: true });\n              const held = await handles[index]!.stat({ bigint: true });\n              if (lexical.isSymbolicLink() || !sameIdentity(fileIdentity(lexical), fileIdentity(held))) {\n                throw new Error(\"ACPX package directory changed while snapshotting\");\n              }\n            }\n            if (runtimeExecutable && privateSnapshot.executable &&\n              `sha256:${privateSnapshot.digests[privateSnapshot.executable]}` !== runtimeExecutable.digest) {\n              throw new Error(\"ACPX runtime snapshot digest mismatch\");\n            }\n          } catch (error) { await privateSnapshot.close(); throw error; }\n        }\n        return commandLease(\n          commandDirectory,\n          basename(commandPath),\n          commandFormat,\n          current.bytes,\n          currentDirectory.handle,\n          currentDependencyAncestors,\n          serverDependencyAncestorCount,\n          serverPackageFormat,\n          dependencyAncestorFormats,","sourceCodeStart":735,"sourceCodeEnd":771,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts#L735-L771","documentation":"On macOS, openCommand creates a private snapshot of the command directory and dependency ancestors, then re-checks each directory via lstat against the identity of the still-open directory handle before using the snapshot. If any path is now a symlink or its device/inode identity differs from the held handle, the tree changed mid-snapshot and the snapshot is rejected to avoid copying partially replaced content.","triggerScenarios":"openCommand on darwin inside the privateSnapshot block, where for any of commandDirectory or the dependency-ancestor paths, lstat returns isSymbolicLink() === true or fileIdentity(lexical) differs from fileIdentity(held) from the open directory handle — a concurrent replacement/rename occurred while createAcpxPrivateSnapshot ran.","commonSituations":"npm/pnpm install re-linking directories during launch; a sync client (iCloud/Dropbox) materializing or replacing directories; another process deleting and recreating the node_modules subtree; test suites that reinstall fixtures between verify and open.","solutions":["Retry: re-run the installation/verification step and call openCommand again once the filesystem is quiet.","Quiesce installers/sync tools so nothing mutates node_modules during agent launch.","Exclude the install tree from iCloud Drive/Dropbox and other path-replacing services.","Investigate which path changed (enable logging around the loop) — a symlink result usually means a package manager re-linked the directory."],"exampleFix":"// before\nconst lease = await verified.openCommand(); // pnpm relink raced the snapshot\n// after\nawait child(helper(\"pnpm install --frozen-lockfile\")); // finish relinking first\nconst lease = await reverified.openCommand();","handlingStrategy":"retry","validationCode":"const st = await fs.promises.lstat(commandDirectory, { bigint: true });\nif (st.isSymbolicLink()) throw new Error(\"command directory is a symlink; fix install layout before opening\");","typeGuard":null,"tryCatchPattern":"try {\n  lease = await verified.openCommand();\n} catch (e) {\n  if (e.message === \"ACPX package directory changed while snapshotting\") {\n    await quiesceInstallers();\n    verified = await verifyQualifiedAcpxInstallation(input);\n    lease = await verified.openCommand();\n  } else throw e;\n}","preventionTips":["Keep the install tree out of iCloud Drive/Dropbox and any path-replacing sync service.","Quiesce installers and CI jobs that touch node_modules during agent launch.","Expect this on macOS only; test snapshot behavior after any change to install tooling."],"tags":["installation-integrity","acpx","toctou","macos","snapshot"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}