Skip to content
Guides · Agent cookbook

How to Give an Agent a Large File Without Filling Its Context

Updated November 1, 2026 .md

Short answer: Put the file in the sandbox and give the agent its path, not its bytes. Upload with sbx scp or fetch it by URL inside the sandbox, then expose small tools that return metadata, a head, a checksum or query rows. On prod, 52 MB files came back as tool results of 73 to 249 bytes.

The pattern#

The model never reads the file. It reads answers about the file.

  1. The bytes go to the sandbox, not to the model: sbx scp from your machine, or curl inside the sandbox when the file has a URL.
  2. The agent gets a path, such as inbox/orders.csv, in its prompt or in a tool result.
  3. The agent calls small tools on that path: probe, head, checksum, query, split. Each returns a few hundred bytes of JSON.
  4. Big results stay files too. A processed file goes back as a URL or a download, not as tool output.

This page measures each step. It doesn't cover streaming input into a running process (send input to a running command) or hosted URLs in depth (give a sandbox port an HTTPS URL).

What we ran#

We tested on prod on 2026-11-01 with one small sandbox (2 vCPU, 4 GB) in us-east. Our test machine had a 32 ms TCP connect time to sandboxer.com. We made two files locally:

  • interview.wav: 272 seconds of synthetic 48 kHz stereo audio with a 3-second gap every 45 seconds, made with ffmpeg. 52,224,078 bytes.
  • orders.csv: 846,139 rows of synthetic orders, 7 columns. 52,000,011 bytes.

A third file came from a URL: a 49,961,641-byte Parquet file of New York City taxi trips from the NYC TLC trip record data.

No LLM key was used. We called each tool handler directly, the way an agent's tool loop would, and measured what it would return to the model.

1. Get the file into the sandbox#

Way in 52 MB file Notes
sbx scp from our machine 1.87 s (WAV), 1.68 s (CSV) Your upload speed sets the pace
curl inside the sandbox 0.69 s (50 MB Parquet) The bytes never touch your machine
SDK files.write, whole file Too big The files API takes up to 5 MiB per file
SDK files.write in 4 MiB parts, joined with cat 2.4 s for 13 parts Works from code when you can't run scp

sbx scp, then a checksum to prove the copy:

id=sb_wo9x0s9c5c
sbx exec $id "mkdir -p ~/inbox"
time sbx scp ./interview.wav $id:/home/user/inbox/
# real    0m1.871s
time sbx scp ./orders.csv $id:/home/user/inbox/
# real    0m1.680s
sbx exec $id "sha256sum ~/inbox/*"
# e4b2d1cd3983ebe47f929e7af59b97480df165a776f0dccdf3294f2606a6736e  /home/user/inbox/interview.wav
# 274209ff95f2896061a7b8881087fcb44111441c1682d835de8809b59160a1a2  /home/user/inbox/orders.csv

Both hashes matched the local files. When the file already has a URL, skip your machine entirely:

sbx exec $id --timeout 120 "cd ~/inbox && curl -sS -o trips.parquet \
  -w 'HTTP %{http_code} %{size_download} bytes in %{time_total} s\n' \
  https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet"
# HTTP 200 49961641 bytes in 0.690292 s

From code without scp, the Python SDK (pip install sandboxer-sdk) can send a large file in parts. The files API takes up to 5 MiB per file, so write 4 MiB parts and join them:

import hashlib, time
from sandboxer import Sandboxer

sandbox = Sandboxer.get("sb_wo9x0s9c5c")
data = open("orders.csv", "rb").read()

t = time.time()
CHUNK = 4 * 1024 * 1024
parts = []
for n, off in enumerate(range(0, len(data), CHUNK)):
    name = f"sdk/parts/{n:03d}"
    sandbox.files.write(name, data[off:off + CHUNK])
    parts.append(name)
run = sandbox.exec("cat sdk/parts/* > sdk/orders.csv && rm -r sdk/parts && sha256sum sdk/orders.csv")
print(f"{len(parts)} parts in {time.time() - t:.1f} s")
print(run.stdout.strip())
13 parts in 2.4 s
274209ff95f2896061a7b8881087fcb44111441c1682d835de8809b59160a1a2  sdk/orders.csv

The joined file's hash matched.

2. Write tools that return answers, not bytes#

Put one small script in the sandbox. Each subcommand prints compact JSON. We copied it in with sbx scp ftools.py $id:/home/user/ and ran python3 -m venv ~/v && ~/v/bin/pip install duckdb (3.3 s). FFmpeg and ffprobe are already in the image.

#!/home/user/v/bin/python
"""Small tools an agent calls on big files. Each prints compact JSON, never the file's bytes."""
import hashlib, json, os, re, subprocess, sys

def file_info(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for block in iter(lambda: f.read(1 << 20), b""):
            h.update(block)
    kind = subprocess.run(["file", "-b", path], capture_output=True, text=True).stdout.strip()
    return {"path": path, "bytes": os.path.getsize(path), "sha256": h.hexdigest(), "type": kind[:80]}

def audio_probe(path):
    out = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
        "format=duration,bit_rate:stream=codec_name,sample_rate,channels",
        "-of", "json", path], capture_output=True, text=True, check=True).stdout
    j = json.loads(out); s = j["streams"][0]
    return {"duration_s": round(float(j["format"]["duration"]), 1), "codec": s["codec_name"],
            "sample_rate": int(s["sample_rate"]), "channels": s["channels"]}

def audio_segments(path, outdir):
    # Find silences, cut at their midpoints, write 16 kHz mono WAVs a transcriber can take one at a time.
    log = subprocess.run(["ffmpeg", "-hide_banner", "-i", path, "-af", "silencedetect=n=-40dB:d=1",
        "-f", "null", "-"], capture_output=True, text=True).stderr
    starts = [float(x) for x in re.findall(r"silence_start: ([\d.]+)", log)]
    ends = [float(x) for x in re.findall(r"silence_end: ([\d.]+)", log)]
    cuts = [round((a + b) / 2, 2) for a, b in zip(starts, ends)]
    os.makedirs(outdir, exist_ok=True)
    subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", path, "-ac", "1", "-ar", "16000",
        "-f", "segment", "-segment_times", ",".join(map(str, cuts)) or "99999",
        f"{outdir}/seg_%03d.wav"], check=True)
    segs = sorted(os.listdir(outdir))
    return {"segments": len(segs), "dir": outdir, "first": segs[0], "last": segs[-1],
            "bytes_total": sum(os.path.getsize(f"{outdir}/{s}") for s in segs)}

def csv_head(path, n=5):
    with open(path) as f:
        return {"lines": [next(f).rstrip("\n") for _ in range(int(n))]}

def sql(query, limit=20):
    import duckdb
    rel = duckdb.sql(query)
    rows = rel.limit(int(limit)).fetchall()
    return {"columns": rel.columns, "rows": [list(map(str, r)) for r in rows]}

if __name__ == "__main__":
    tool, *args = sys.argv[1:]
    print(json.dumps(globals()[tool](*args), separators=(",", ":")))

The limit in sql matters: a model that writes SELECT * still gets at most 20 rows.

3. What the agent gets back#

We ran each tool through a wrapper that prints the output, its size and its run time inside the sandbox:

#!/bin/bash
# measure.sh <tool> <args...>
cd ~
s=$EPOCHREALTIME
out=$(./ftools.py "$@")
e=$EPOCHREALTIME
echo "$out"
printf '[%d bytes, %.2f s]\n' "$(printf '%s' "$out" | wc -c)" "$(awk "BEGIN{print $e - $s}")"
sbx exec $id --timeout 120 "~/measure.sh audio_probe inbox/interview.wav"
$ file_info inbox/interview.wav
{"path":"inbox/interview.wav","bytes":52224078,"sha256":"e4b2d1cd…6736e","type":"RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, stereo 48000 Hz"}
[210 bytes, 0.10 s]
$ audio_probe inbox/interview.wav
{"duration_s":272.0,"codec":"pcm_s16le","sample_rate":48000,"channels":2}
[73 bytes, 0.12 s]
$ audio_segments inbox/interview.wav work/segments
{"segments":7,"dir":"work/segments","first":"seg_000.wav","last":"seg_006.wav","bytes_total":8704546}
[101 bytes, 1.12 s]
$ csv_head inbox/orders.csv 4
{"lines":["order_id,created_at,region,sku,qty,unit_price_cents,customer_id","1,2026-09-11T04:25:00Z,us-east,SKU-0037,9,6367,C095864",…]}
[249 bytes, 0.05 s]

The agent-side handler is a few lines with the Python SDK. Passing a list to exec quotes each argument, so a model's SQL can't break the shell command:

import json
from sandboxer import Sandboxer

sandbox = Sandboxer.get("sb_wo9x0s9c5c")

def call(tool, *args):
    """What the agent's tool handler does: run one small tool on the file in the sandbox."""
    run = sandbox.exec(["/home/user/ftools.py", tool, *map(str, args)], timeout_seconds=120)
    if run.exit_code != 0:
        return {"error": run.stderr[-500:]}          # keep errors small too
    return json.loads(run.stdout)

call("sql", "SELECT region, count(*) AS orders, round(sum(qty * unit_price_cents) / 100) AS revenue_usd "
            "FROM 'inbox/orders.csv' GROUP BY region ORDER BY revenue_usd DESC")
call("sql", "SELECT sku, sum(qty) AS units FROM 'inbox/orders.csv' GROUP BY sku ORDER BY units DESC", 3)
call("sql", "SELECT count(*) AS trips, round(avg(trip_distance), 2) AS avg_miles, "
            "round(avg(total_amount), 2) AS avg_total FROM 'inbox/trips.parquet'")
{"columns":["region","orders","revenue_usd"],"rows":[["ap-south","169590","212815376.0"],["us-east","168990","212597864.0"],…]}
[233 bytes, 0.31 s round trip]
{"columns":["sku","units"],"rows":[["SKU-0248","11354"],["SKU-0338","11305"],["SKU-0165","11260"]]}
[99 bytes, 0.28 s round trip]
{"columns":["trips","avg_miles","avg_total"],"rows":[["2964624","3.65","26.8"]]}
[80 bytes, 0.17 s round trip]

Round trips are from our machine and include the API call.

Tool call File size Result size Share of the file
audio_probe 52,224,078 B 73 B 0.00014%
audio_segments 52,224,078 B 101 B 0.00019%
file_info (sha256) 52,224,078 B 210 B 0.0004%
csv_head 4 lines 52,000,011 B 249 B 0.0005%
sql group by region 52,000,011 B 233 B 0.00045%
sql over Parquet 49,961,641 B 80 B 0.00016%

4. Long steps: detach and poll#

Transcribing seven segments takes longer than one exec should wait. Start the loop detached and give the agent a status tool. Our stub writes one JSON line per segment; the text field is where your transcriber's output goes (for timings with a real model, see transcribe audio with whisper.cpp):

#!/bin/bash
# transcribe_all.sh <segments-dir> <out.jsonl>: one line per segment.
# Stub: swap the "text" line for your transcriber (for example whisper-cli -f "$f").
cd ~
: > "$2"
for f in "$1"/seg_*.wav; do
  dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f")
  vol=$(ffmpeg -hide_banner -i "$f" -af volumedetect -f null - 2>&1 | sed -n 's/.*mean_volume: \(.*\) dB/\1/p')
  text=null
  printf '{"segment":"%s","duration_s":%.1f,"mean_db":%s,"text":%s}\n' "$(basename "$f")" "$dur" "$vol" "$text" >> "$2"
done
echo "done: $(wc -l < "$2") segments"
sbx exec $id --detach "~/transcribe_all.sh work/segments work/transcript.jsonl"
# {"ok":true,"type":"command.started",…,"processId":1025,…}
sbx exec $id --status 1025
# {…,"exitCode":0,…,"status":"exited",…,"stdout":"done: 7 segments\n",…}
sbx exec $id "head -3 ~/work/transcript.jsonl; wc -c ~/work/transcript.jsonl"
{"segment":"seg_000.wav","duration_s":43.5,"mean_db":-17.1,"text":null}
{"segment":"seg_001.wav","duration_s":45.0,"mean_db":-17.3,"text":null}
{"segment":"seg_002.wav","duration_s":45.0,"mean_db":-17.3,"text":null}
503 /home/user/work/transcript.jsonl

The stub finished in 0.9 seconds. The agent reads the transcript a few lines at a time, or runs a search tool over it, the same way it reads the CSV.

5. Return a derived file as a URL or a download#

A 16 kHz mono Opus copy of the interview took 1.9 seconds and came out at 995,516 bytes. The agent returns where it is, not what's in it:

sbx exec $id --timeout 120 "cd ~ && mkdir -p out && ffmpeg -v error -y -i inbox/interview.wav \
  -ac 1 -ar 16000 -c:a libopus -b:a 24k out/interview-16k.opus"
sbx exec $id --detach "cd ~/out && python3 -m http.server 8000"
sbx host $id 8000
# {…,"url":"https://fresh-agile-dove-8000.box.sandboxer.com?_token=…",…}

The URL is private by default. Without the token, and with it:

curl -sS -o /dev/null -w "HTTP %{http_code} %{size_download} bytes\n" https://fresh-agile-dove-8000.box.sandboxer.com/interview-16k.opus
# HTTP 401 583 bytes
curl -sSL -c jar -b jar -o interview-16k.opus -w "HTTP %{http_code} %{size_download} bytes in %{time_total} s\n" "$url_with_token"
# HTTP 200 995516 bytes in 1.114867 s

The first tokened request answers 302 and sets a cookie, so use curl -L with a cookie jar. Treat the tokened URL like a password. Your code can also pull the file directly: sandbox.files.download("out/interview-16k.opus") returned all 995,516 bytes in 0.45 seconds (up to 50 MiB), and sbx scp $id:/home/user/out/interview-16k.opus . took 1.4 seconds. All three copies were byte-identical.

6. The same tools through MCP#

If your agent uses the Sandboxer MCP server, you don't need custom tool definitions. Its exec tool runs the same script on the path. These are real calls to the local sbx mcp server over stdio:

{"name": "exec", "arguments": {"sandboxId": "sb_wo9x0s9c5c", "command": "~/ftools.py audio_probe inbox/interview.wav"}}
→ "Exit code 0.\n\nstdout:\n{\"duration_s\":272.0,\"codec\":\"pcm_s16le\",\"sample_rate\":48000,\"channels\":2}"
  (whole result with structuredContent: 336 bytes, 0.22 s)

{"name": "exec", "arguments": {"sandboxId": "sb_wo9x0s9c5c", "command": "~/ftools.py sql \"SELECT region, count(*) AS orders FROM 'inbox/orders.csv' GROUP BY 1 ORDER BY 2 DESC\" 2"}}
→ "Exit code 0.\n\nstdout:\n{\"columns\":[\"region\",\"orders\"],\"rows\":[[\"ap-south\",\"169590\"],[\"eu-west\",\"169341\"]]}"
  (368 bytes, 0.27 s)

{"name": "read_file", "arguments": {"sandboxId": "sb_wo9x0s9c5c", "path": "inbox/orders.csv"}}
→ isError: "Files over 5 MiB: use GET /sandboxes/{sandboxId}/artifacts (50 MiB) or scp. (file_too_large)"

read_file refusing the 52 MB file is the safety net working: the MCP server won't put it into the model's context. Tell the agent in its prompt to use exec on paths, and use detached: true with command_status for anything that runs past the timeout.

Limits to plan around#

Limit Value What we saw
sbx scp Your disk size 52 MB in under 2 s
SDK / files API / MCP read_file, write_file Up to 5 MiB per file 4 MiB parts worked
SDK files.download 50 MiB 995,516 bytes in 0.45 s
sbx exec wait 30 s default, 1–600 s with --timeout Use --detach past that
MCP exec wait 120 s default, at most 600 s Use detached: true past that
Command output kept 8 MiB per stream for waiting commands Return paths or rows, not whole files

Never let a tool print a whole file: return a path, a count or a few rows.

FAQ#

Can't I just paste a large CSV into the prompt?#

Not usefully. 52 MB of CSV is far more text than a context window holds, and even a slice crowds out the instructions. A sql tool returned the answer to a group-by over 846,139 rows in 233 bytes.

How do I get a big file into a sandbox quickly?#

If it has a URL, download it inside the sandbox: 50 MB took 0.69 seconds. From your machine, sbx scp moved 52 MB in under 2 seconds from ours; your upload speed sets the pace.

Can the SDK upload a 50 MB file?#

Not in one call: the files API takes up to 5 MiB per file. Writing 4 MiB parts and joining them with cat in the sandbox took 2.4 seconds for 52 MB, and the checksum matched. For anything large, sbx scp is simpler.

Does the file survive if the sandbox stops?#

Yes. Files under /home/user are in the snapshot that sbx stop writes, and sbx resume brings them back, so the agent can pick up the same path later.

How does the agent send a processed file back to a user?#

Host the folder and return the URL, or have your code call files.download (up to 50 MiB) or sbx scp. In our run a private hosted URL answered 401 without its token and served the 995,516-byte file with it.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide