Get files in and out of a sandbox¶
There is no file API. No sb.files.write(), no upload endpoint, no download URL. The sandbox has exactly one door — exec — and every file you move goes through it or through the sandbox's own network connection.
That is a real limitation and this page is the honest workaround, not a feature tour. Read the size limits first: they decide which of the three patterns you need.
Size limits, read this first¶
| Direction | Practical limit | What happens past it |
|---|---|---|
| Writing a file into the sandbox | a few hundred KB per run() | The request body grows; splitting into chunks gets slow |
Reading via sb.run() / a job | 32 MiB per stream | Cut, with truncated: true |
| Either, at any size | unlimited | Let the sandbox fetch/push it over the network |
Output is capped at 32 MiB per stream
Past that, the rest is dropped and truncated is true. For bigger files, use the network pattern below.
Pattern 1 — write a small text file in¶
A quoted heredoc. <<'EOF' with the marker in quotes stops the shell expanding $variables and backticks inside your content, which matters when the content is code an LLM wrote.
import { Sandbox } from "@sistemo/sdk";
const script = `console.log(JSON.stringify(process.argv.slice(2)));`;
const sb = await Sandbox.create({ stack: "node" });
try {
await sb.run(`cat > /tmp/main.js <<'SISTEMO_EOF'\n${script}\nSISTEMO_EOF`);
console.log((await sb.run("node /tmp/main.js a b")).stdout);
} finally {
await sb.close();
}
A heredoc breaks if your content contains the marker
If the file you are writing happens to contain a line that is exactly SISTEMO_EOF, the heredoc ends early and the rest becomes shell commands. For content you did not write — and LLM output is content you did not write — use base64 instead. It has no escaping rules at all.
Pattern 2 — base64 for anything you did not write¶
The only safe way to move arbitrary bytes through a shell. It costs ~33% more bytes and it never breaks on quotes, newlines, markers or binary.
import base64
from sistemo import Sandbox
with Sandbox() as sb:
sb.run("head -c 100000 /dev/urandom > /tmp/blob")
# Guard the 32 MiB stdout cap: base64 inflates by ~4/3.
size = int(sb.run("stat -c %s /tmp/blob").stdout.strip())
assert size * 4 / 3 < 32 * 1024 * 1024, "too big — use the network pattern"
b64 = sb.run("base64 -w0 /tmp/blob").stdout
data = base64.b64decode(b64)
print(len(data))
base64 -w0 matters: without it base64 wraps at 76 columns and you have to strip the newlines before decoding.
Pattern 3 — let the sandbox move it over the network¶
For anything above roughly a megabyte, do not push bytes through exec at all. Sandboxes have outbound internet access (that is how pip install works), so the fastest path in and out is the sandbox talking to your own storage directly.
from sistemo import Sandbox
with Sandbox(stack="python") as sb:
# In: the sandbox pulls it.
sb.run("curl -fsSL -o /tmp/input.csv 'https://example.com/presigned-get-url'", timeout=120)
sb.run("python3 -c \"import csv;print(sum(1 for _ in open('/tmp/input.csv')))\"")
# Out: the sandbox pushes it.
sb.run("curl -fsS -T /tmp/result.parquet 'https://example.com/presigned-put-url'", timeout=120)
Use presigned URLs with a short expiry. A long-lived credential passed into a sandbox is a credential you have handed to the code you did not trust enough to run on your own machine — the whole reason you are here.
What the sandbox cannot reach
Outbound SMTP is blocked, so a sandbox cannot be used to send mail. The guest also cannot see your other sandboxes, the host, or any cloud metadata service.
Pattern 4 — keep the files instead of moving them¶
If the same data is needed by run after run, stop copying it. Attach a volume once and it survives the sandbox being destroyed and recreated.
A volume appears in the guest as the next drive — /dev/vdb for the first data disk — and nothing mounts it for you. Every run that wants it mounts it first:
A volume can only be attached to a stopped machine, so it cannot be attached to a fresh Sandbox() — that one is already running when you get it. Create the machine, stop it, attach, start it: Persist data with volumes walks through it, and Volumes has the endpoints.
Once it is attached, mount and write in one command:
⚠ Chain them with &&. As two separate commands, a failed mount is followed by a write that succeeds — onto the machine's own root disk under /mnt/data, which is thrown away with the machine. Nothing errors, and the data is gone. Destroy the machine, attach the same volume to a new one, mount again — log.txt is still there.
Choosing¶
| Your situation | Use |
|---|---|
| Writing a script you control, < 100 KB | Heredoc (pattern 1) |
| Writing anything an LLM produced | Base64 (pattern 2) |
| Reading a result you know is small | cat, after checking stat -c %s |
| Anything ≥ 1 MB, either direction | Presigned URL (pattern 3) |
| The same data on every run | A volume (pattern 4) |
Related¶
- Exec — the endpoint all of this runs through
- Persist data with volumes
- Long-running work