paperclipai/paperclip · error · Error
ACPX package directory changed during snapshot
Error message
ACPX package directory changed during snapshot
What it means
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.
Source
Thrown at packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts:116
if (++filesCopied > 30_000)
throw new Error("ACPX package snapshot exceeds its file bound");
const before = await lstat(source, { bigint: true });
if (before.isSymbolicLink()) {
const canonical = await realpath(source);
const mapped = mapPath(canonical);
// Package-manager links to unqualified packages do not grant import authority.
if (mapped) await symlink(mapped, target);
return;
}
if (!within(root, await realpath(source)))
throw new Error("ACPX snapshot escaped its package");
if (before.isDirectory()) {
await mkdir(target, { mode: 0o700 });
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 }))View on GitHub (pinned to 01ad858492)
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
Example fix
// before: watcher active during snapshot const watcher = startWatcher(pkgRoot); await snapshot([pkgRoot]); // after watcher.stop(); await snapshot([pkgRoot]);
Defensive patterns
Strategy: retry
Validate before calling
const before = await fs.stat(dir);
await new Promise(r => setTimeout(r, 250));
const after = await fs.stat(dir);
if (before.mtimeMs !== after.mtimeMs) throw new Error('directory is actively changing'); Try / catch
try {
await createAcpxPrivateSnapshot({ roots });
} catch (e) {
if (e.message.includes('directory changed during snapshot')) {
await waitForWritersToFinish(roots);
await createAcpxPrivateSnapshot({ roots });
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ACPX file ended during snapshot
- ACPX module changed before snapshot
- ACPX module changed during snapshot
- ACPX package root changed during snapshot
- ${name} must be a JSON object
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/cb4d8c689dbec028.
Report an issue: GitHub.