paperclipai/paperclip · error
Outbound CreateOS transfers cannot run post-upload commands.
Error message
Outbound CreateOS transfers cannot run post-upload commands.
What it means
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.
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.
Example fix
// before
const op = { files: [...], postUploadCommands: [{ cmd: "chmod +x /work/run.sh" }] };
await syncFiles({ direction: "out", operations: [op] });
// after
const op = { files: [...] }; // no postUploadCommands for "out"
await syncFiles({ direction: "out", operations: [op] }); Defensive patterns
Strategy: validation
Validate before calling
if (direction === "out") {
for (const op of operations) delete op.postUploadCommands; // or reject if present
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- CreateOS transfer requires an absolute host path.
- Unsupported CreateOS transfer kind.
- A full lowercase source SHA is required.
- A reusable lease cannot be replaced and reacquired in the…
- A reusable lease handoff requires an execution workspace…
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/db98ef4499b35905.
Report an issue: GitHub.
Appendix: source
Thrown at packages/plugins/sandbox-providers/createos/src/file-sync.ts:103
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.
const parent = direction === "out" ? path.dirname(local) : os.tmpdir();
await fs.mkdir(parent, { recursive: true });
const temp = await fs.mkdtemp(path.join(parent, ".paperclip-createos-"));
const transferFile = path.join(temp, "data");
try {
if (direction === "in") {
let source = local;View on GitHub (pinned to 3f1d897a7c)