Skip to content

How to run an agent's code in a sandbox?

Code an agent wrote can loop forever, fill memory or call out to the network. Run it in a sandbox, so that it stays off your files and you set its limits.

Write the agent’s code into a fresh sandbox, run it, and read what it printed:

Terminal window
printf 'print("hello from the agent")\n' > main.py
shard create --name agent --memory 512MiB python:3.12-slim
shard cp main.py agent:/tmp/main.py
shard exec agent python /tmp/main.py
echo "exit code $?"
shard remove --force agent

The TypeScript and Python examples reach shard serve at the address in SHARD_REMOTE, with the API key in SHARD_API_KEY.

The code runs in the sandbox, not in your process. It sees the image’s files and the file you wrote.

shard exec prints the command’s output as it runs. When the output stream is complete, it exits with the command’s exit code. The SDKs return a result with these fields:

TypeScript Python What it holds
exitCode exit_code the command’s exit code
signal signal always none, since a signal shows in the exit code
stdout, stderr stdout, stderr the newest output
lostBytes lost_bytes output the daemon dropped before any client read it

A nonzero exit code is a result, not an error: the SDK call succeeds, and you read the failure from exitCode or exit_code.

A command ended by a signal reports the exit code 128 plus the signal number, so KILL gives 137.

The result keeps the newest 8 MiB of stdout and stderr combined. Set outputLimitBytes (output_limit_bytes) to keep more or less. To get every chunk as it arrives, pass onStdout and onStderr (on_stdout and on_stderr).

Pass the input on stdin, or set environment variables for one command:

Terminal window
echo '{"n": 3}' | shard exec -i --env MODE=test agent python /tmp/main.py

To read a file the code wrote, run shard cp agent:/tmp/out.json ./out.json. In TypeScript, call sandbox.files.read("/tmp/out.json", { encoding: "utf8" }). In Python, sandbox.files.read("/tmp/out.json") returns bytes.

A command runs until it exits or something ends it. Without -t, Ctrl+C on shard exec only detaches from the output. A disconnect or a cancelled wait also leaves the command running. Set a time bound when you start it:

Terminal window
shard exec agent timeout -k 5 30 python /tmp/main.py

The CLI has no time bound of its own, so the example uses timeout from the image. It sends TERM after 30 seconds and KILL five seconds later if the command still runs. It reports 124 after the TERM timeout, or 137 if KILL is needed. The SDKs start the command in the background and send KILL when the wait times out. A command killed that way reports 137; a command that finishes first keeps its own exit code.

Set the bounds when you create the sandbox:

Terminal window
shard create --name agent --memory 1GiB --vcpus 2 --disk 4GiB python:3.12-slim

A write past the disk bound fails with ENOSPC, and the sandbox keeps running. If you set no disk bound, it is 10 GiB. If you set no memory bound, it is 512 MiB on vz and firecracker, and there is no bound on the other providers. On vz and firecracker the sandbox also gets a 2 GiB swap file, which comes out of the disk bound; --swap 0 turns it off. For details on each bound, see the provider notes.

A sandbox without a policy can reach the internet, but not a private network. To cut off the internet too, store a policy that denies everything and create the sandbox with it:

Terminal window
shard policy create --deny any offline
shard create --name agent --memory 512MiB --policy offline python:3.12-slim

Every name lookup in that sandbox then fails, and the host drops or refuses every outgoing connection. To let the code reach one API, put an --allow rule for that host before --deny any. See egress for the rules and the decision log.

When the code needs an API key, give it a placeholder and let the host put the key in. See secrets.

Each create starts from the image’s files. Commands in one sandbox share its files and installed packages, so a later command sees whatever an earlier one left behind. Run the steps of one task in one sandbox, and give an unrelated task a new sandbox so that it starts clean.

A sandbox remains until you remove it, even after every command in it has ended. When the task is done, remove it with shard remove --force, sandbox.remove({ force: true }) or sandbox.remove(force=True).