paperclipai/paperclip · error
CreateOS transfer path escapes the workspace.
Error message
CreateOS transfer path escapes the workspace.
What it means
After rejecting non-absolute/insecure paths, assertRemotePath normalizes the path and requires the result to be exactly /paperclip-workspace or inside /paperclip-workspace/. This catches paths that are syntactically absolute but resolve outside the sanctioned workspace root — the containment boundary for all CreateOS file transfers.
Solutions
- Rewrite the destination so its normalized form is under /paperclip-workspace/ (or equals ROOT).
- Use path.posix.join(ROOT, relative) instead of string concatenation to build remote paths.
- Validate paths with assertRemotePath yourself in dev/test before shipping calls that transfer files.
- Beware prefix bugs: /paperclip-workspace-evil is rejected; rely on the normalized startsWith(ROOT + '/') rule.
Example fix
// before const remote = "/tmp/results/out.json"; // after const remote = "/paperclip-workspace/results/out.json";
Defensive patterns
Strategy: validation
Validate before calling
function underWorkspace(p) {
const n = path.posix.normalize(p);
return n === "/paperclip-workspace" || n.startsWith("/paperclip-workspace/");
}
// assert before every transfer call
if (!underWorkspace(remote)) throw new Error("destination must be inside /paperclip-workspace"); Type guard
function isInsideWorkspace(p) {
const n = path.posix.normalize(p);
return n === "/paperclip-workspace" || n.startsWith("/paperclip-workspace/");
} Try / catch
try {
await syncFiles(lease, transfers);
} catch (e) {
if (e.message.includes("escapes the workspace")) {
log.error("remote path outside /paperclip-workspace", { transfers });
}
throw e;
} Prevention
- Use path.posix.join(ROOT, relative) rather than string concatenation for destinations.
- Run a dev-time assertion that all hardcoded remote paths normalize inside ROOT.
- Remember prefix traps: /paperclip-workspace-* siblings are rejected.
When it happens
Trigger: Passing a path like /etc/passwd, /tmp/x, or /paperclip-workspace-sibling/file whose normalized form does not start with /paperclip-workspace/; also paths relying on normalization tricks that escape ROOT.
Common situations: Hardcoding destination paths meant for a different sandbox provider; writing to host-style temp directories; confusing the sandbox root with the host root; prefix string-check bugs when constructing paths (e.g. /paperclip-workspace-backup).
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
- CreateOS transfer requires a confined absolute sandbox path.
- sandbox runtime asset key is not a simple path segment
- Access denied
- ACPX runtime executable escapes its package
- ACPX snapshot escaped its package
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/07684ed42ca65c94.
Report an issue: GitHub.
Appendix: source
Thrown at packages/plugins/sandbox-providers/createos/src/file-sync.ts:19
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;
const inside = (entryPath: string) => !path.posix.isAbsolute(entryPath) &&
!entryPath.split("/").includes("..") && !entryPath.includes("\\") && !entryPath.includes("\0");
await tar.t({ file, strict: true, onReadEntry(entry) {View on GitHub (pinned to 3f1d897a7c)