paperclipai/paperclip · error

CreateOS archive contains unsafe entries or exceeds the…

Error message

CreateOS archive contains unsafe entries or exceeds the extraction limit.

What it means

validateArchive scans the tar archive built for upload into the sandbox. If any entry is unsafe (absolute link targets, backslashes in linkpath, link targets resolving outside the extraction root, missing linkpath) or the archive exceeds the entry/size extraction limit, the transfer is refused before upload. This prevents symlink attacks and zip-bomb style extractions inside the sandbox.

Solutions

  1. Remove or re-point symlinks in the source tree (or exclude them with the sync ignore patterns) before syncing.
  2. Use the excluded/pattern options to skip directories known to contain symlinks (node_modules, .git).
  3. Shrink the transfer: sync fewer files or split large uploads so the archive stays under the extraction limit.
  4. Regenerate the archive with POSIX-compliant tooling so linkpaths use forward slashes and relative targets.

Example fix

// before
await syncFiles(lease, [{ local: "./node_modules", remote: "/paperclip-workspace/app/node_modules" }]);
// after
await syncFiles(lease, [{ local: "./src", remote: "/paperclip-workspace/app/src" }], { exclude: ["node_modules"] });
Defensive patterns

Strategy: validation

Validate before calling

import tar from "tar-stream";
function hasUnsafeLinks(fileList) {
  // pre-screen source tree for symlinks before archiving
  return fileList.some((f) => {
    const st = fs.lstatSync(f);
    return st.isSymbolicLink();
  });
}
if (hasUnsafeLinks(filesToSync)) console.warn("symlinks present; they may be rejected by validateArchive");

Try / catch

try {
  await syncFiles(lease, transfers, { exclude: ["node_modules", ".git"] });
} catch (e) {
  if (e.message.includes("unsafe entries or exceeds the extraction limit")) {
    log.error("archive rejected: check symlinks and archive size");
  }
  throw e;
}

Prevention

When it happens

Trigger: syncFiles produces an archive containing a symlink whose linkpath is absolute or contains backslashes, a symlink target that normalizes outside the base directory, a hardlink/symlink entry with no linkpath, or an archive exceeding configured entry-count/size limits.

Common situations: Syncing directories that contain symlinks (node_modules, build outputs, git worktrees); archives generated on Windows with backslash separators; excluding the limit via very large or deeply nested source trees; malicious or corrupted archives in untrusted workspaces.

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@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/01292132d0567625. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugins/sandbox-providers/createos/src/file-sync.ts:49

export async function validateArchive(file: string): Promise<number> {
  let invalid = false;
  let bytes = 0;
  let files = 0;
  const inside = (entryPath: string) => !path.posix.isAbsolute(entryPath) &&
    !entryPath.split("/").includes("..") && !entryPath.includes("\\") && !entryPath.includes("\0");
  await tar.t({ file, strict: true, onReadEntry(entry) {
    bytes += entry.size;
    if (bytes > 10 * 1024 ** 3 || !inside(entry.path)) invalid = true;
    if (!["File", "OldFile", "Directory", "SymbolicLink", "Link"].includes(entry.type)) invalid = true;
    if (entry.type === "File" || entry.type === "OldFile") files++;
    if (entry.type === "SymbolicLink" || entry.type === "Link") {
      if (!entry.linkpath) { invalid = true; return; }
      const base = entry.type === "SymbolicLink" ? path.posix.dirname(entry.path) : ".";
      const target = path.posix.normalize(path.posix.join(base, entry.linkpath));
      if (path.posix.isAbsolute(entry.linkpath) || entry.linkpath.includes("\\") || !inside(target)) invalid = true;
    }
  } });
  if (invalid) throw new Error("CreateOS archive contains unsafe entries or exceeds the extraction limit.");
  return files;
}

function excluded(name: string, patterns: string[]): boolean {
  name = name.replace(/^\.\//, "").replace(/\/$/, "");
  return patterns.some((pattern) => [pattern, `${pattern}/**`, `**/${pattern}`, `**/${pattern}/**`]
    .some((glob) => path.matchesGlob(name, glob)));
}

export async function syncFiles(
  client: CreateosClient,
  params: PluginEnvironmentSyncInParams,
  direction: "in" | "out",
  signal: AbortSignal,
): Promise<PluginEnvironmentSyncResult> {
  const id = identifier(params.lease.providerLeaseId);
  const operations: PluginEnvironmentSyncResult["operations"] = [];
  const run = async (command: string, cwd = ROOT, timeoutMs?: number) => {

View on GitHub (pinned to 3f1d897a7c)