{"record":{"id":"db98ef4499b35905","repo":"paperclipai/paperclip","slug":"outbound-createos-transfers-cannot-run-post-upload-commands","errorCode":null,"errorMessage":"Outbound CreateOS transfers cannot run post-upload commands.","messagePattern":"Outbound CreateOS transfers cannot run post-upload commands\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/file-sync.ts","lineNumber":103,"sourceCode":"    const response = await client.request(`/sandboxes/${id}/files?path=${encodeURIComponent(remote)}`, { signal });\n    if (!response.body) throw new Error(\"CreateOS file download has no body.\");\n    await pipeline(response.body, createWriteStream(local, { flags: \"wx\", mode }), { signal });\n  };\n\n  // Validate every mapping before beginning side effects. Host paths are\n  // orchestrator-authored and checked by its source/target-root guard.\n  for (const operation of params.operations) {\n    for (const mapping of operation.files) {\n      if (![\"file\", \"directory\"].includes(mapping.kind)) throw new Error(\"Unsupported CreateOS transfer kind.\");\n      if (!path.isAbsolute(direction === \"in\" ? mapping.sourcePath : mapping.targetPath)) throw new Error(\"CreateOS transfer requires an absolute host path.\");\n      if (mapping.mode != null && (!Number.isInteger(mapping.mode) || mapping.mode < 0 || mapping.mode > 0o777)) throw new Error(\"Invalid CreateOS file mode.\");\n      assertRemotePath(direction === \"in\" ? mapping.targetPath : mapping.sourcePath);\n    }\n    for (const command of operation.postUploadCommands ?? []) {\n      assertRemotePath(command.cwd ?? ROOT);\n      if (command.timeoutMs != null && (!Number.isInteger(command.timeoutMs) || command.timeoutMs < 1 || command.timeoutMs > 86_400_000)) throw new Error(\"Invalid CreateOS transfer timeout.\");\n    }\n    if (direction === \"out\" && operation.postUploadCommands?.length) throw new Error(\"Outbound CreateOS transfers cannot run post-upload commands.\");\n  }\n\n  for (const operation of params.operations) {\n    let bytesTransferred = 0;\n    let filesTransferred = 0;\n    for (const mapping of operation.files) {\n      signal.throwIfAborted();\n      const local = direction === \"in\" ? mapping.sourcePath : mapping.targetPath;\n      const remote = direction === \"in\" ? mapping.targetPath : mapping.sourcePath;\n      const scratch = `/tmp/paperclip-createos-transfer-${randomUUID()}`;\n      // Outbound temporary files are on the target filesystem for atomic rename.\n      const parent = direction === \"out\" ? path.dirname(local) : os.tmpdir();\n      await fs.mkdir(parent, { recursive: true });\n      const temp = await fs.mkdtemp(path.join(parent, \".paperclip-createos-\"));\n      const transferFile = path.join(temp, \"data\");\n      try {\n        if (direction === \"in\") {\n          let source = local;","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/file-sync.ts#L85-L121","documentation":"Post-upload commands only make sense for inbound transfers (files pushed into the sandbox). When direction is \"out\" and an operation still lists postUploadCommands, the plugin rejects the request before transferring anything, since there is nothing uploaded to post-process.","triggerScenarios":"syncFiles is invoked with direction \"out\" and any operation in params.operations has a non-empty postUploadCommands array — often from reusing a shared operation template built for inbound syncs.","commonSituations":"Copy-pasted operation configs between inbound and outbound sync flows; a generic sync builder that always appends postUploadCommands; refactors that changed the transfer direction without pruning commands.","solutions":["Remove postUploadCommands from operations used in outbound (sandbox-to-host) transfers.","Split the work into an outbound transfer followed by a separate inbound operation if you need remote command execution.","Conditionally attach postUploadCommands only when direction === \"in\" in your sync builder."],"exampleFix":"// before\nconst op = { files: [...], postUploadCommands: [{ cmd: \"chmod +x /work/run.sh\" }] };\nawait syncFiles({ direction: \"out\", operations: [op] });\n// after\nconst op = { files: [...] }; // no postUploadCommands for \"out\"\nawait syncFiles({ direction: \"out\", operations: [op] });","handlingStrategy":"validation","validationCode":"if (direction === \"out\") {\n  for (const op of operations) delete op.postUploadCommands; // or reject if present\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Build operation payloads per-direction instead of sharing templates","Only attach postUploadCommands when direction is \"in\"","Add a lint/test asserting outbound ops carry no postUploadCommands"],"tags":["validation","file-transfer","conflicting-options"],"backgroundTag":"conflicting-config-options","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}