paperclipai/paperclip · critical

Daytona sync ${label} path escapes the workspace remote dir:

Error message

Daytona sync ${label} path escapes the workspace remote dir: ${candidate}

What it means

Thrown by `assertConfinedSandboxPath` (file-sync.ts:121) — the second fail-closed check — when the candidate path is absolute and free of `..` but does NOT equal the workspace `remoteDir` and does not start with `remoteDir + "/"`. I.e. the path is absolute but outside the permitted workspace root, so a transfer would read/write outside confinement.

Source

Thrown at packages/plugins/sandbox-providers/daytona/src/file-sync.ts:121

 * orchestrator's own confinement. Every sandbox-side path (the sync target for
 * inbound, the sync source for outbound) MUST canonicalize inside the workspace
 * remote dir; absolute escapes and `..` traversal are rejected fail-closed before
 * any bytes move. Sandbox paths on the server are POSIX.
 */
export function assertConfinedSandboxPath(remoteDir: string, candidate: string, label: string): void {
  const normalizedRoot = path.posix.normalize(remoteDir);
  const normalized = path.posix.normalize(candidate);
  if (
    !path.posix.isAbsolute(normalized) ||
    normalized === ".." ||
    normalized.includes("/../") ||
    normalized.endsWith("/..")
  ) {
    throw new Error(`Daytona sync ${label} path is not a confined absolute path: ${candidate}`);
  }
  const prefix = normalizedRoot.endsWith("/") ? normalizedRoot : `${normalizedRoot}/`;
  if (normalized !== normalizedRoot && !normalized.startsWith(prefix)) {
    throw new Error(`Daytona sync ${label} path escapes the workspace remote dir: ${candidate}`);
  }
}

async function withHostTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-daytona-sync-"));
  try {
    return await fn(dir);
  } finally {
    await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
  }
}

/**
 * Build a host-side tarball of a directory, mirroring the runtime's own
 * `createTarballFromDirectory`: archive top-level entries by name (no "." self
 * entry), suppress AppleDouble/xattr sidecars, honor `exclude`, and reproduce the
 * `followSymlinks` → `-h` mapping so the native path is observationally identical
 * to the base64 fallback's tar.

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Scope every path to the current workspace's `remoteDir` prefix; build target/source paths as `remoteDir`-rooted.
  2. Confirm the `remoteDir` passed to the sync matches the workspace the paths belong to.
  3. Reject any caller-supplied absolute path that is not under the active remote dir.

Example fix

// before
cwd: "/shared/build" // outside remoteDir
// after
cwd: `${remoteDir}/build`
Defensive patterns

Strategy: validation

Validate before calling

import path from "node:path";
function assertInsideRoot(remoteDir, candidate) {
  const root = path.posix.normalize(remoteDir);
  const n = path.posix.normalize(candidate);
  const prefix = root.endsWith("/") ? root : `${root}/`;
  if (n !== root && !n.startsWith(prefix)) {
    throw new Error(`path escapes workspace remote dir: ${candidate}`);
  }
  return n;
}

Type guard

function isInsideRemoteRoot(remoteDir: string, candidate: string): boolean {
  const root = path.posix.normalize(remoteDir);
  const n = path.posix.normalize(candidate);
  const prefix = root.endsWith("/") ? root : `${root}/`;
  return n === root || n.startsWith(prefix);
}

Prevention

When it happens

Trigger: A sync mapping or post-upload command `cwd` points to an absolute path outside the workspace remote dir — e.g. `/etc/...`, `/root/...`, or a different workspace's directory. The lexical check (536) passed (absolute, no traversal) but this prefix check rejects the escape.

Common situations: Cross-workspace path leakage (using another workspace's remoteDir), a hard-coded absolute path in config, or untrusted input that resolves under a sibling root rather than the current workspace's remote dir.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/c0b3210d5055349a. Report an issue: GitHub.