paperclipai/paperclip · error

CreateOS transfer requires a confined absolute sandbox path.

Error message

CreateOS transfer requires a confined absolute sandbox path.

What it means

assertRemotePath validates every remote path used in CreateOS file transfers. A path must be POSIX-absolute, contain no NUL byte, and no '..' segment; otherwise it cannot be confined and the transfer is refused. This is the first-line containment check before path normalization.

Solutions

  1. Convert the remote path to an absolute POSIX path rooted at /paperclip-workspace before calling the transfer API.
  2. Normalize the path with path.posix.normalize and strip '..' segments yourself before submission.
  3. Never pass raw user input as the remote path; map it under ROOT with path.posix.join(ROOT, relativePart).
  4. On Windows hosts, replace backslashes with forward slashes and drop drive letters before use.

Example fix

// before
await syncFiles(lease, [{ local: file, remote: "output/result.json" }]);
// after
const remote = path.posix.join("/paperclip-workspace", "output/result.json");
await syncFiles(lease, [{ local: file, remote }]);
Defensive patterns

Strategy: validation

Validate before calling

function toSafeRemotePath(p) {
  const posix = String(p).replaceAll("\\", "/");
  const normalized = path.posix.normalize(posix);
  if (!path.posix.isAbsolute(normalized) || normalized.includes("\0")) throw new Error("remote path must be absolute");
  return path.posix.join("/paperclip-workspace", path.posix.relative("/paperclip-workspace", normalized) || "");
}

Type guard

function isConfinedRemotePath(p) {
  const n = path.posix.normalize(p);
  return path.posix.isAbsolute(n) && !n.includes("\0") && (n === "/paperclip-workspace" || n.startsWith("/paperclip-workspace/"));
}

Try / catch

try {
  await syncFiles(lease, transfers);
} catch (e) {
  if (e.message.includes("confined absolute sandbox path")) {
    log.error("bad remote path", { transfers });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling syncFiles/run/remoteGuard with a relative path (e.g. 'foo/bar'), a Windows-style path ('C:\\foo'), a path containing a NUL character, or a segment equal to '..' (e.g. '/paperclip-workspace/../etc').

Common situations: Joining local relative paths and passing them as remote paths by mistake; user-supplied destination paths not normalized; interpolating OS-specific paths on a Windows host into a POSIX sandbox.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/5138b06be28e74b4. Report an issue: GitHub.

Appendix: source

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

import path from "node:path";
import os from "node:os";
import { randomUUID } from "node:crypto";
import { createReadStream, createWriteStream, promises as fs } from "node:fs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import * as tar from "tar";
import type { PluginEnvironmentSyncInParams, PluginEnvironmentSyncResult } from "@paperclipai/plugin-sdk";
import { CreateosClient, identifier } from "./client.js";
import { execute, shellQuote } from "./execute.js";

const ROOT = "/paperclip-workspace";

export function assertRemotePath(value: string): void {
  if (!path.posix.isAbsolute(value) || value.includes("\0") || value.split("/").includes("..")) {
    throw new Error("CreateOS transfer requires a confined absolute sandbox path.");
  }
  const normalized = path.posix.normalize(value);
  if (normalized !== ROOT && !normalized.startsWith(`${ROOT}/`)) throw new Error("CreateOS transfer path escapes the workspace.");
}

function remoteGuard(candidate: string): string {
  assertRemotePath(candidate);
  // Re-check symlinks inside the sandbox immediately before use. A missing
  // canonicalizer fails the command rather than weakening containment.
  return `root=$(realpath -- ${shellQuote(ROOT)}) && test "$root" = ${shellQuote(ROOT)} && ` +
    `resolved=$(realpath -m -- ${shellQuote(candidate)}) && ` +
    `case "$resolved" in "$root"|"$root"/*) ;; *) exit 1 ;; esac`;
}

export async function validateArchive(file: string): Promise<number> {
  let invalid = false;
  let bytes = 0;
  let files = 0;

View on GitHub (pinned to 3f1d897a7c)