# How to upload a folder to a sandbox Call `sbx.files.upload(localDir, "/workspace/project")`. The whole folder is sent as one compressed archive, and it keeps its permissions. **On Runtime a folder upload is one call and one archive, not a loop of file writes.** Scripts stay runnable because each file keeps its mode, and single large files go in parallel 1 MiB chunks, each checked by SHA-256, resuming after a dropped connection. There is no size limit beyond the sandbox's disk. ## Upload a project and run it ```ts import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Sandbox } from "withruntime"; const project = await mkdtemp(join(tmpdir(), "project-")); await writeFile(join(project, "main.py"), "print('hi')\n"); await writeFile(join(project, "run.sh"), "python3 main.py\n", { mode: 0o755 }); await using sbx = await Sandbox.create(); await sbx.files.upload(project, "/workspace/project"); console.log(await sbx.files.list("/workspace/project")); const run = await sbx.exec("./run.sh", { cwd: "/workspace/project" }); console.log(run.stdout); ``` ```python import pathlib import tempfile from withruntime import Sandbox project = pathlib.Path(tempfile.mkdtemp()) (project / "main.py").write_text("print('hi')\n") with Sandbox.create() as sbx: sbx.files.upload(str(project), "/workspace/project") for entry in sbx.files.list("/workspace/project"): print(entry["type"], entry["size"], entry["path"]) print(sbx.exec(["python3", "main.py"], cwd="/workspace/project").stdout) ``` From a terminal, `cp` takes a sandbox path as `:/path`. It copies directories whole: ```bash id=$(runtime sandboxes create) mkdir -p project && echo "print('hi')" > project/main.py runtime sandboxes cp ./project "${id}:/workspace/project" runtime sandboxes files "${id}" /workspace --depth 2 runtime sandboxes stop "${id}" ``` ## From any language, over HTTP The SDKs and the CLI send the folder to one API route. A language with no SDK (PHP, Ruby, Go, a shell script) can do the same with a tar archive and a PUT. The sandbox unpacks the archive with its own `tar` as the bytes arrive. It keeps modes and links, and it creates the target folder: ```bash no-run tar -czf site.tar.gz -C ./site . curl -sS -X PUT "https://api.withruntime.com/v1/sandboxes/${ID}/files/archive?path=/workspace/site" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}" --data-binary @site.tar.gz ``` One request carries up to 1 MiB of archive. A larger archive starts an upload with `POST …/files/archive/uploads`. Then send 1 MiB parts in order to `PUT …/files/archive/uploads/{uploadId}?offset=`. Finish with `:commit`, which returns once every file is in place. A part sent twice after a lost response is not written twice. Nothing is staged on the sandbox's disk. To download, use `GET …/files/archive?path=/workspace/site&gzip=true`. It streams an archive of any size ([files in the API guide](/docs/api#files)). ## One file, or many small ones Use `files.write` for a single file that you already hold in memory. It creates parent directories and replaces the file atomically. The `mode` option sets its permissions: ```ts import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); await sbx.files.write("/workspace/bin/start.sh", "#!/bin/sh\necho ready\n", { mode: 0o755 }); await sbx.files.write("/workspace/data/input.csv", "a,b\n1,2\n"); console.log(await sbx.files.exists("/workspace/data/input.csv")); ``` ```python from withruntime import Sandbox with Sandbox.create() as sbx: sbx.files.write("/workspace/bin/start.sh", "#!/bin/sh\necho ready\n", mode=0o755) sbx.files.write("/workspace/data/input.csv", "a,b\n1,2\n") print(sbx.files.exists("/workspace/data/input.csv")) ``` For a whole tree, `upload` sends one archive instead of a write per file. ## Sizes, speeds and costs | What | Figure | | -------------------- | ----------------------------------------------------------------------------------- | | How a folder travels | One tar archive, unpacked by the sandbox as it arrives, permissions and links kept | | Large single files | Parallel 1 MiB chunks, each checked by SHA-256, resumed after a dropped connection | | Size limit | None beyond the disk; over HTTP, 1 MiB a request | | Default disk | 4 GiB, about 2.5 GiB free (measured 24 September 2026); ask for more with `diskMiB` | | Disk without credit | Up to 10 GiB | | Disk speed | At least about 40 MB/s, up to about 1 GB/s on a quiet server | | Default mode | 0o644 for `write` without `mode`; `upload` keeps each file's own | | Cost | Nothing per upload; the disk is part of the sandbox | ## Mistakes to avoid - **Filling the default disk.** The system image and installed packages share `diskMiB`. For a large repository or dataset, create the sandbox with `Sandbox.create({ diskMiB: 16_384 })` (`disk_mib` in Python) ([disk, CPU and memory](/docs/sandbox-environment#disk-cpu-and-memory)). - **Relative sandbox paths.** SDK paths are absolute. `/workspace` is the sandbox user's home. `sudo` reaches the rest. - **Uploading the same tree on every run.** When every sandbox needs the same code or packages, bake them into a [custom image](/docs/images) once. Or keep data on a [volume](/docs/javascript#volumes) that outlives sandboxes. - **A script that will not run.** A file made with `write` and no `mode` gets 0o644. Pass `mode: 0o755`, or upload the folder, which keeps the mode it had. ## Related - [Download files from a sandbox](/how-to/download-files-from-a-sandbox) to get results back. - [Watch files for changes](/how-to/watch-files-for-changes) while an agent edits the upload. - [Coding agent sandbox](/use-cases/coding-agent-sandbox): check out or upload a repository, then let the agent work. - [Files](/docs/javascript#files) in the SDK reference. Facts on this page were checked on 26 October 2026.