Why Writing a Big File Into a Sandbox Keeps Failing

A big write goes through fine on your laptop. The same write against a Modal sandbox comes back with ValueError: embedded null byte. Against another backend the error is stranger: write_file reports success and the file on disk is zero bytes. You blame the network, permissions, a stale cache — and the actual reason is maddeningly boring: the entire payload was passed to the shell as a command-line argument, and command-line arguments have a length limit and are visible to every process on the machine. A fix merged on 2026-09-26 (PR #124206) ends that.
The root cause: stdin data in argv
cat file | bash -c '...' is normal shell work, but the sandbox backends took a shortcut: the payload destined for a command’s stdin got concatenated into the bash -c argument itself (the code calls it the heredoc path).
That works fine until it meets two hard limits:
| Limit | Number | What happens |
|---|---|---|
| NUL bytes | An argument cannot contain \0 |
ValueError: embedded null byte, the write fails outright |
| Argument length | Linux MAX_ARG_STRLEN is about 128 KiB; Modal is tighter (~64 KiB exec-arg limit) |
Larger writes fail outright on Linux or Modal |
The reproduction in the PR is blunt: a 160 KiB payload containing a NUL raises embedded null byte on main, and the argv element it built is 165003 bytes long. Read the second half again — even without the NUL, that size is already past the line.
There is a second effect that is easy to miss: argv is public. Any process on the same machine can read another process’s command line. Concatenating file contents — which may hold keys, tokens or user data — into argv quietly parks them in the process table.
The fix: each backend declares how it hands over stdin
The patch introduces an explicit mode declaration (tools/environments/base.py):
# How execute() hands stdin to _run_bash: "pipe" (process stdin, default),
# "payload" (passed through as stdin_data; the backend transports it via its
# SDK -- Modal/managed Modal stream it, Daytona/Vercel stage a file) or
# "heredoc" (embedded in the command; no built-in backend, plugins only).
_stdin_mode: str = "pipe" # "pipe" | "payload" | "heredoc"
Across the three built-in backends:
| Backend | What it does now |
|---|---|
| Modal | _stdin_mode = "payload"; stdin is streamed through the SDK, byte-exact (surrogateescape), in chunks (1 MiB each, staying under the legacy server’s 2 MB cap) |
| Daytona | The payload is uploaded to a private 0600 file, then fed to the command as exec 0< f; rm -f -- f; cmd |
| Vercel | Same shape: bytes land at a private path, permissions are set, stdin is redirected from it |
Daytona’s version is a little cleaner still: the bytes go straight through the SDK with no host temp file, and empty stdin skips staging entirely.
Look closely at the redirect prefix — two actions, in a deliberate order:
exec 0< <staged-path> || exit $? # the shell takes over the descriptor
rm -f -- <staged-path> || exit $? # then the file is unlinked
<command>
exec before rm means the file is gone from disk for the whole duration of the command while the data stays readable through the already-open descriptor. Both steps became shared helpers on BaseEnvironment (_staged_stdin_path, _redirect_stdin_from_file); the old _embed_stdin_heredoc path survives only for plugin backends.
The cancel logic got rewritten too
The original cleanup had two real defects: any SDK error stopped the whole sandbox, and every cancel stopped it twice. The patch splits the state into three (cancelled / staged / dispatched) and holds it under the environment lock:
- a cancel landing after the upload but before dispatch scrubs the staged file (Vercel overwrites, Daytona deletes) and then stops once;
- an ordinary SDK error no longer stops the sandbox;
- a cancelled command is never dispatched.
What it means for you
- No configuration changes. Each backend swapped its transport; callers see nothing.
- Cases that used to fail silently or loudly — content with NUL bytes, writes above roughly 128 KiB — now land byte-exact.
- If you maintain a sandbox backend plugin,
_embed_stdin_heredocis still there, but new backends should declarepayloadmode for the reasons in that table. - The three PRs merged on 2026-09-26, after the v0.21.5 release tag, so they are on
main.hermes updategets them.
The bigger pattern
This work has an earlier chapter: PRs #122012 / #122011 fixed a heredoc bound to the wrong command, which is the other face of the byte-exact write problem — a file that looks written and holds the wrong bytes. The argv fix here covers “it never got written at all” plus “another process could read it”. Together they make one point: when you ship data to a remote environment, the command line is the last place it belongs.
If you lean on remote sandboxes, read on: how media files get out of a sandbox, what a shared Docker container means for parallel agents, and the rest of the window on the v0.21.5 release page.