paperclipai/paperclip · error · Error

ACPX snapshot escaped its package

Error message

ACPX snapshot escaped its package

What it means

During snapshot copy, for non-symlink entries the library verifies via realpath that the source resolves within the admitted package root; if not, the snapshot 'escaped its package' and it throws. This blocks path traversal — entries that escape the root via symlinks resolved as real directories or other aliasing — from being inducted into the private snapshot.

Source

Thrown at packages/paperclip-runner/src/drivers/acpx/private-snapshot.ts:109

      : null;
  };
  const copy = async (
    source: string,
    target: string,
    root: string,
  ): Promise<void> => {
    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,
    );

View on GitHub (pinned to 01ad858492)

Solutions

  1. Verify the package root is the true canonical parent of all contents (no entries resolving outside it)
  2. Use a real (non-symlinked) package directory, or an install layout where packages own their files (npm's isolated layout rather than symlinked hoisting)
  3. Check for concurrent modification racing the lstat/realpath window and re-run when the tree is stable
  4. Audit package contents for outbound symlinks before admitting the root

Example fix

// before: root is a symlinked hoist location
roots: ['/project/node_modules/pkg']
// after: root is the real package store path
roots: ['/project/node_modules/.pnpm/pkg@1.0.0/node_modules/pkg']
Defensive patterns

Strategy: validation

Validate before calling

const real = await fs.realpath(source);
if (!real.startsWith(await fs.realpath(root) + path.sep)) {
  throw new Error(`entry escapes package root: ${source} -> ${real}`);
}

Try / catch

try {
  await createAcpxPrivateSnapshot({ roots: [root] });
} catch (e) {
  if (e.message.includes('escaped its package')) {
    throw new Error(`Root ${root} contains entries resolving outside it; use the real package path`);
  }
  throw e;
}

Prevention

When it happens

Trigger: copy() encounters a directory/file whose realpath(source) is outside the `root` package root — e.g. a hard-linked tree, a bind-mounted path, or a directory symlink resolved before the isSymbolicLink branch (variants where lstat reports a real dir but the canonical path leaves the root), or a symlink swapped in between lstat and realpath.

Common situations: A package containing symlinks that lstat sees as directories due to a race; copy-on-write/hardlink-style installs (pnpm) where entries resolve outside the claimed root; misconfigured package root that doesn't actually contain the entry.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/6805802da5a0c93f. Report an issue: GitHub.