Skip to content

Blog

Move files in and out of agent sandboxesuploads, volumes and buckets

Upload the inputs, let the sandbox fetch big public data itself, keep reused data on a volume or bucket, and download only what changed.

· The Runtime team

10 min read

Read as Markdown

Runtime (withruntime.com) moves a folder into a sandbox as one compressed archive. The archive is unpacked as it arrives. Volumes that outlive sandboxes are billed at about $0.11 per GiB per 30-day month. Runtime mounts your own bucket, and the sandbox never holds its key. An agent's work begins with files it did not make and ends with files someone else needs. Get the transfer wrong, and the result is slow starts, a server of yours that relays gigabytes for no reason, or a results folder full of node_modules. This post sorts an agent's files by where they come from and how often they are reused. It gives each kind its own route.

Which route fits which files?

Two questions decide the route: how big are the files, and will the next sandbox need them too?

Files Route Why
A user's upload, a prompt's attachment files.write from your server Small, and already in your hands
A project folder files.upload of the folder One archive, permissions and links kept
A public dataset or a release download The sandbox fetches it itself Your server never relays the bytes
Private data already in a bucket A bucket mount, read-only No copy, and no key inside the sandbox
Data many sandboxes read: weights, a corpus A volume, attached read-only to each Copied in once, read by any number
A cache one agent reuses run after run A read-write volume, or an image Survives the sandbox
Results someone reads files.download of what changed Only the agent's output comes back
Results other systems read Written to a mounted bucket Lands where they already look

The rest of this post covers them in the order that an agent's task uses them: in, during, and out.

How do you hand a task to the agent?

Put everything the task needs in one folder, and upload it in one call. A folder travels as one compressed archive. The sandbox's own tar unpacks it as it arrives, so a thousand small files cost about the same as one archive. A single file is sent in parallel, in 1 MiB chunks. Each chunk is checked by SHA-256, and an upload resumes after a dropped connection.

Then the agent works. At the end, you want its output, not the inputs you already have. List the folder, keep the files modified after the hand-over, and bring back only those:

TypeScriptimport { mkdtemp, writeFile } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";import { Sandbox } from "withruntime";// The task: a folder on your machine with the inputs and the instructions.const task = await mkdtemp(join(tmpdir(), "task-"));await writeFile(join(task, "sales.csv"), "region,revenue\nnorth,1200\nsouth,950\n");await writeFile(join(task, "TASK.md"), "Summarize revenue by region in out/summary.md\n");await using sbx = await Sandbox.create({ labels: { task: "revenue-summary" } });await sbx.files.upload(task, "/workspace/task"); // in: one archive, unpacked as it arrivesconst handedOver = Date.now() - 2_000; // a little slack for the two clocks// The agent works here, through exec and files calls. Its output, for example:await sbx.files.write("/workspace/task/out/summary.md", "north: 1,200\nsouth: 950\n");// Out: only what changed since the hand-over.const entries = await sbx.files.list("/workspace/task", { depth: 4 });const changed = entries.filter((e) => e.type === "file" && Date.parse(e.modifiedAt) >= handedOver);for (const file of changed) {  await sbx.files.download(file.path, join(task, file.path.slice("/workspace/task/".length)));  console.log("brought back", file.path, `${file.size} bytes`);}console.log(`${changed.length} of ${entries.length} entries changed`);
Pythonimport osimport tempfileimport timefrom datetime import datetimefrom withruntime import Sandbox# The task: a folder on your machine with the inputs and the instructions.task = tempfile.mkdtemp(prefix="task-")with open(os.path.join(task, "sales.csv"), "w") as f:    f.write("region,revenue\nnorth,1200\nsouth,950\n")with open(os.path.join(task, "TASK.md"), "w") as f:    f.write("Summarize revenue by region in out/summary.md\n")with Sandbox.create(labels={"task": "revenue-summary"}) as sbx:    sbx.files.upload(task, "/workspace/task")  # in: one archive, unpacked as it arrives    handed_over = time.time() - 2  # a little slack for the two clocks    # The agent works here, through exec and files calls. Its output, for example:    sbx.files.write("/workspace/task/out/summary.md", "north: 1,200\nsouth: 950\n")    # Out: only what changed since the hand-over.    entries = sbx.files.list("/workspace/task", depth=4)    changed = [e for e in entries if e["type"] == "file"               and datetime.fromisoformat(e["modifiedAt"].replace("Z", "+00:00")).timestamp() >= handed_over]    for file in changed:        local = os.path.join(task, file["path"].removeprefix("/workspace/task/"))        os.makedirs(os.path.dirname(local), exist_ok=True)        sbx.files.download(file["path"], local)        print("brought back", file["path"], f"{file['size']} bytes")    print(f"{len(changed)} of {len(entries)} entries changed")

A time check is enough when the agent writes new files. When it edits files in place, and you need to know which edits changed the content, compare content hashes instead. The recorder in see what your agent did does this. For a code task, git diff inside the sandbox is the best option: it shows the change as text, ready to review.

Should big data pass through your server?

No. If the data is on the public internet, let the sandbox download it directly. The data arrives faster, your server stays out of the path, and nothing large is held in your process's memory on the way:

TypeScriptimport { Sandbox } from "withruntime";const url = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet";await using sbx = await Sandbox.create({  diskMiB: 16_384,  network: { internet: true, allow: ["d37ci6vzurychx.cloudfront.net"] }, // this host and no other});const r = await sbx.exec(`curl -fsSL --retry 3 -o trips.parquet '${url}' && ls -l trips.parquet`, {  cwd: "/workspace",  timeoutMs: 600_000,});console.log(r.exitCode, r.stdout.trim());
Pythonfrom withruntime import Sandboxurl = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet"with Sandbox.create(disk_mib=16_384,                    network={"internet": True, "allow": ["d37ci6vzurychx.cloudfront.net"]}) as sbx:    r = sbx.exec(f"curl -fsSL --retry 3 -o trips.parquet '{url}' && ls -l trips.parquet",                 cwd="/workspace", timeout_ms=600_000)    print(r.exit_code, r.stdout.strip())

Two settings in that code matter. The allow list names the one host the download needs. If a model later reads instructions inside the data, it still cannot send anything to another host. The disk is sized for the data: a sandbox's default disk is 4 GiB, and a large file plus its unpacked or converted copy fills that quickly (size CPU and memory).

What about private data in a bucket?

Mount it. A bucket in S3, R2 or Google Cloud Storage appears as a folder. The agent reads it with ordinary file tools. You store the bucket's key once, as a secret. The sandbox gets a placeholder. The real key is added to requests only on the way to the bucket's host. The agent can read the data, but it cannot read the key:

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create();await sbx.mounts.add({  provider: "s3",  bucket: "acme-research",  prefix: "datasets/2026-q3",  region: "us-east-1",  path: "/data",  secret: "RESEARCH_BUCKET",  readOnly: true, // the agent reads; it cannot delete or overwrite});console.log((await sbx.exec("ls -la /data | head")).stdout);
Pythonfrom withruntime import Sandboxwith Sandbox.create() as sbx:    sbx.mounts.add(provider="s3", bucket="acme-research", prefix="datasets/2026-q3",                   region="us-east-1", path="/data", secret="RESEARCH_BUCKET",                   read_only=True)  # the agent reads; it cannot delete or overwrite    print(sbx.exec("ls -la /data | head").stdout)

Keep readOnly and prefix. A prefix limits the agent to the data for this task. Read-only means that an agent that decides to clean up cannot delete anything in your bucket. For results that other systems pick up, mount a second, writable prefix for output only. A mount lasts through a pause and a wake, and it ends when the sandbox stops (mount an S3 bucket).

When is a volume the right answer?

Use a volume when the same bytes are needed by many sandboxes, or by the next sandbox. A volume is a disk that outlives sandboxes. It has two modes:

  • Read-write, one sandbox at a time. A package cache, a database file, a model's checkpoint directory. Each run picks up where the last one stopped.
  • Read-only copies, any number at the same time. Model weights or a reference corpus that every worker reads. Copy the data in once. Then attach it to a hundred sandboxes with mode: "snapshot". None of them can change it.
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();const weights = await runtime.volumes.create({ sizeMiB: 40_960, name: "weights-v3" });const workers = await Promise.all(  [1, 2, 3, 4].map(() =>    runtime.sandboxes.create({      volumes: [{ volumeId: weights.id, path: "/models", mode: "snapshot" }],    }),  ),);console.log(workers.map((w) => w.id));await Promise.all(workers.map((w) => w.stop()));
Pythonfrom withruntime import Runtimeruntime = Runtime()weights = runtime.volumes.create(40_960, name="weights-v3")workers = [runtime.sandboxes.create(volumes=[{"volumeId": weights["id"], "path": "/models", "mode": "snapshot"}])           for _ in range(4)]print([w.id for w in workers])for w in workers:    w.stop()

A volume is billed for its full size from the moment it is created, whether or not it holds data. Runtime backs it up off its server every day. A volume lives on one server, and a sandbox that uses it is placed on that server. Before you choose a volume, note this trade-off: a sandbox with volumes cannot be snapshotted. If your agent relies on snapshots to save and restore its sandbox, keep the reused data in a custom image or a bucket mount instead (an undo button for AI agents).

Volume, image or bucket?

All three keep data beyond one sandbox. The difference is how often it changes and who writes it:

Keep it in Changes Written by Works with snapshots
A custom image Rarely: when you rebuild it Your build Yes
A volume Every run, or never (read-only) The sandbox using it No
A bucket mount Any time, from anywhere Any system with the key Mount it in each new sandbox

Tools and libraries belong in an image. They change when you decide to rebuild, and every sandbox from the image starts with them installed. Data that one run writes and the next run reads belongs on a volume. Data that other systems also read or write belongs in a bucket.

How should the model itself find the files?

Tell it. The task message should name the input folder, the output folder, and what belongs in each: "the data is in /workspace/task, write results to /workspace/task/out, do not change the inputs". A model that has to discover the layout spends its first turns on ls and find. A model that does not know where output belongs scatters it across the disk. That defeats the download step above. An agent connected over MCP reads, writes, and lists files with runtime_sandboxes_files_read, runtime_sandboxes_files_write and runtime_sandboxes_files_list. It can also read a range of lines from a large file, not the whole file.

What limits should you plan around?

A few numbers save a debugging session:

  • Writes outside /workspace are limited to 1 MiB through the files API. Write large files to /workspace, then move them with a command.
  • Four uploads and four downloads run at the same time per sandbox. Extra transfers wait in line. Upload a folder rather than a thousand files one by one.
  • Downloads are checked. A read that arrives short is read again. If the retry is also short, the download fails instead of returning part of a file. A downloaded folder appears in place only after all of it has arrived.
  • A folder download refuses links that point outside the folder so nothing in a sandbox can write anywhere else on your machine.

In short

  • Upload a task's inputs as one folder, and bring back only the files that changed since the hand-over.
  • Let the sandbox fetch large public data itself, with an allow list naming only the source.
  • Mount private buckets read-only with a prefix; the sandbox never holds the key.
  • Use a volume for data many sandboxes read or the next run reuses, and remember that a sandbox with volumes cannot be snapshotted.
  • Put tools in an image, run-to-run state on a volume, and shared data in a bucket.

Run it on Runtime

Runtime costs 42% to 88% less than fourteen other sandbox providers for an agent that mostly waits on a model (compare costs). Every Runtime sandbox has a files API with checked transfers. Folder uploads travel as one archive. Bucket mounts keep keys out of the sandbox. Volumes outlive the sandbox that uses them. Read the storage guide, then create an account at withruntime.com and hand your agent its first task folder.

Keep reading.

Cut your cloud billby 75%.

No card to start. Billed by the second after.

Start free, no card