paperclipai/paperclip · error

Invalid CreateOS file mode.

Error message

Invalid CreateOS file mode.

What it means

Each mapping may carry an optional POSIX file `mode`. The plugin validates it is an integer within 0–0o777 (0–511 decimal) before executing the transfer. Non-integer values, negatives, or modes above 0777 (e.g. setuid/sticky bits included) are rejected with this error.

Solutions

  1. Supply mode as a plain integer between 0 and 0o777, e.g. 0o600 or 420.
  2. If you have a full lstat mode, mask it: mode & 0o777.
  3. Ensure the value is a number type, not a string, before calling syncFiles.

Example fix

// before
const mode = fs.statSync(local).mode; // e.g. 33188
// after
const mode = fs.statSync(local).mode & 0o777; // e.g. 420 (0o644)
Defensive patterns

Strategy: validation

Validate before calling

const isValidMode = (m: unknown) =>
  Number.isInteger(m) && (m as number) >= 0 && (m as number) <= 0o777;
if (mapping.mode != null && !isValidMode(mapping.mode)) throw new Error("bad mode");

Type guard

const isPosixMode = (v: unknown): v is number =>
  typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 0o777;

Prevention

When it happens

Trigger: A mapping sets mode to a non-integer (e.g. "644" string), a negative number, NaN, or a value > 0o777 such as 0o100644 (lstat-style mode) or 420 (decimal misinterpretation of 0644).

Common situations: Passing fs.Stats.mode (which includes file-type bits) directly instead of just permission bits; storing modes as decimal strings in config; JSON round-trips turning numbers into strings.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

        duplex: "half", headers: { "Content-Type": "application/octet-stream" }, signal,
      };
      const response = await client.request(`/sandboxes/${id}/files?path=${encodeURIComponent(remote)}`, init);
      await response.body?.cancel();
    } finally { source.destroy(); }
  };
  const download = async (remote: string, local: string, mode = 0o600) => {
    const response = await client.request(`/sandboxes/${id}/files?path=${encodeURIComponent(remote)}`, { signal });
    if (!response.body) throw new Error("CreateOS file download has no body.");
    await pipeline(response.body, createWriteStream(local, { flags: "wx", mode }), { signal });
  };

  // Validate every mapping before beginning side effects. Host paths are
  // orchestrator-authored and checked by its source/target-root guard.
  for (const operation of params.operations) {
    for (const mapping of operation.files) {
      if (!["file", "directory"].includes(mapping.kind)) throw new Error("Unsupported CreateOS transfer kind.");
      if (!path.isAbsolute(direction === "in" ? mapping.sourcePath : mapping.targetPath)) throw new Error("CreateOS transfer requires an absolute host path.");
      if (mapping.mode != null && (!Number.isInteger(mapping.mode) || mapping.mode < 0 || mapping.mode > 0o777)) throw new Error("Invalid CreateOS file mode.");
      assertRemotePath(direction === "in" ? mapping.targetPath : mapping.sourcePath);
    }
    for (const command of operation.postUploadCommands ?? []) {
      assertRemotePath(command.cwd ?? ROOT);
      if (command.timeoutMs != null && (!Number.isInteger(command.timeoutMs) || command.timeoutMs < 1 || command.timeoutMs > 86_400_000)) throw new Error("Invalid CreateOS transfer timeout.");
    }
    if (direction === "out" && operation.postUploadCommands?.length) throw new Error("Outbound CreateOS transfers cannot run post-upload commands.");
  }

  for (const operation of params.operations) {
    let bytesTransferred = 0;
    let filesTransferred = 0;
    for (const mapping of operation.files) {
      signal.throwIfAborted();
      const local = direction === "in" ? mapping.sourcePath : mapping.targetPath;
      const remote = direction === "in" ? mapping.targetPath : mapping.sourcePath;
      const scratch = `/tmp/paperclip-createos-transfer-${randomUUID()}`;
      // Outbound temporary files are on the target filesystem for atomic rename.

View on GitHub (pinned to 3f1d897a7c)