paperclipai/paperclip · error · Error
ACPX module changed before snapshot
Error message
ACPX module changed before snapshot
What it means
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.
Source
Thrown at packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts:130
directories.push(target);
for (const entry of await readdir(source))
await copy(join(source, entry), join(target, entry), root);
if (!same(before, await lstat(source, { bigint: true })))
throw new Error("ACPX package directory changed during snapshot");
return;
}
if (!before.isFile() || before.size > 16n * 1024n * 1024n)
throw new Error("ACPX module must be a bounded regular file");
bytesCopied += Number(before.size);
if (bytesCopied > MAX_PACKAGE_SNAPSHOT_BYTES)
throw new Error("ACPX package snapshot exceeds its byte bound");
const handle = await open(
source,
constants.O_RDONLY | constants.O_NOFOLLOW,
);
try {
if (!same(before, await handle.stat({ bigint: true })))
throw new Error("ACPX module changed before snapshot");
const bytes = await readSnapshotBytes(handle, Number(before.size));
if (
!same(before, await handle.stat({ bigint: true })) ||
!same(before, await lstat(source, { bigint: true }))
) {
throw new Error("ACPX module changed during snapshot");
}
await writeFile(target, bytes, { flag: "wx", mode: 0o400 });
digests[target] = digest(bytes);
} finally {
await handle.close();
}
};
try {
for (let index = 0; index < sourceRoots.length; index++) {
await copy(sourceRoots[index]!, roots[index]!, sourceRoots[index]!);
if (
!same(View on GitHub (pinned to 01ad858492)
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
Example fix
// before: snapshot during deploy await deploy(); // rewrites package files await snapshot(roots); // after await deploy(); await waitForIdle(roots); await snapshot(roots);
Defensive patterns
Strategy: retry
Validate before calling
const st1 = await fs.stat(file);
const st2 = await fs.stat(file);
if (st1.ino !== st2.ino || st1.mtimeMs !== st2.mtimeMs || st1.size !== st2.size) {
throw new Error('file is being rewritten; defer snapshot');
} Try / catch
try {
await createAcpxPrivateSnapshot({ roots });
} catch (e) {
if (e.message.includes('changed before snapshot')) {
await new Promise(r => setTimeout(r, 1000));
await createAcpxPrivateSnapshot({ roots });
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Package manager performing an in-place upgrade during snapshot; build tooling rewriting output files; container image layer being updated while the runner snapshots.
Related errors
- ACPX file ended during snapshot
- ACPX package directory changed during snapshot
- ACPX module changed during snapshot
- ACPX package root changed during snapshot
- ACPX ${agent} runtime executable changed while it was verifi
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/330d5d037fdd821f.
Report an issue: GitHub.