Create a sandbox, run commands and move files
You’ll create a sandbox, run commands in it, copy a script in and copy a result out. You need a shard server: see Quickstart: server.
The TypeScript and Python tabs reach the server through shard serve, at the address in SHARD_REMOTE and with the API key in SHARD_API_KEY. See Quickstart: client.
# Create a sandbox from an image. Prints the sandbox's ID.shard create --name agent --memory 512MiB python:3.12
# Run a command in it. Prints Python 3.12 and its patch version.shard exec agent python --version
# Write a script on your machine, copy it in and run it. Prints 4.echo 'print(2 + 2)' > main.pyshard cp main.py agent:/srv/main.pyshard exec agent python /srv/main.py
# Write a file in the sandbox and copy it out. The cat prints done.shard exec agent sh -c 'echo done > /srv/out.txt'shard cp agent:/srv/out.txt ./out.txtcat out.txt
# List your sandboxes, then remove this one.shard listshard remove --force agentimport { readFile, writeFile } from "node:fs/promises";import { Shard } from "useshards";
const shard = new Shard();try { // Create a sandbox from an image. const sandbox = await shard.create({ name: "agent", image: "python:3.12", memoryMiB: 512 }); try { // Run a command in it. Prints Python 3.12 and its patch version. console.log((await sandbox.exec(["python", "--version"])).stdout);
// Write a script on your machine, copy it in and run it. Prints 4. await writeFile("main.py", "print(2 + 2)\n"); await sandbox.files.upload("main.py", "/srv/main.py"); console.log((await sandbox.exec(["python", "/srv/main.py"])).stdout);
// Write a file in the sandbox and copy it out. Prints done. await sandbox.exec("echo done > /srv/out.txt"); await sandbox.files.download("/srv/out.txt", "out.txt"); console.log(await readFile("out.txt", "utf8")); } finally { // Remove the sandbox. await sandbox.remove({ force: true }); }} finally { shard.close();}from pathlib import Path
from useshards import Shard
with Shard() as shard: # Create a sandbox from an image. sandbox = shard.create("python:3.12", name="agent", memory_mib=512) try: # Run a command in it. Prints Python 3.12 and its patch version. print(sandbox.exec(["python", "--version"]).stdout)
# Write a script on your machine, copy it in and run it. Prints 4. Path("main.py").write_text("print(2 + 2)\n") sandbox.files.upload("main.py", "/srv/main.py") print(sandbox.exec(["python", "/srv/main.py"]).stdout)
# Write a file in the sandbox and copy it out. Prints done. sandbox.exec("echo done > /srv/out.txt") sandbox.files.download("/srv/out.txt", "out.txt") print(Path("out.txt").read_text()) finally: # Remove the sandbox. sandbox.remove(force=True)A new sandbox runs nothing but shard’s own supervisor, and it stays running until you stop it. exec runs one command and returns its output and exit code.
A string command runs through /bin/sh -c, so it can use pipes and redirects. A list runs as is, with no shell.
Where a copy lands
Section titled “Where a copy lands”shard cp works like docker cp. When the destination is a directory in the sandbox, or ends with /, the source goes inside it under its own name, so shard cp ./project agent:/srv/ creates /srv/project.
A copy out works the same way on your machine. If a local path contains a colon, start it with ./ or /.
The SDKs use the exact path you give them: uploadDir("./project", "/srv/project") gives /srv/project the same contents as ./project. A path in the sandbox must always be absolute, because shard has no default directory for a file call.
Read and write without a local file
Section titled “Read and write without a local file”The SDKs can write and read a sandbox file directly. The CLI has no such verb, so from a shell you write a local file and copy it in.
# Write the file on your machine, then copy it in.printf '{"n": 3}\n' > input.jsonshard cp input.json agent:/srv/input.json// Write the file in the sandbox, then read it back as text.await sandbox.files.write("/srv/input.json", '{"n": 3}\n');const text = await sandbox.files.read("/srv/input.json", { encoding: "utf8" });# Write the file in the sandbox, then read it back as text.sandbox.files.write("/srv/input.json", '{"n": 3}\n')text = sandbox.files.read_text("/srv/input.json")Without an encoding option, files.read returns bytes: a Uint8Array in TypeScript and bytes in Python. These are all the file calls in the SDKs:
| TypeScript | Python | What it does |
|---|---|---|
write, read |
write, read, read_text |
put or get bytes or text |
upload, download |
upload, download |
copy one file between a local path and the sandbox |
uploadDir, downloadDir |
upload_dir, download_dir |
copy a directory |
stat, list |
stat, list |
get the type, size, mode, owner and modification time of a path, or of each entry in a directory |
mkdir, remove |
mkdir, remove |
create a directory, or delete a path; with recursive, delete a directory that is not empty |
write, upload and mkdir take parents to create any missing directories, and mode to set the permission bits.
shard cp, upload and download keep the source’s ordinary permission bits, but not setuid or setgid.
Who owns a written file
Section titled “Who owns a written file”A copy into the sandbox runs as a user inside the sandbox. That user is the sandbox’s user unless you set --user on shard cp or user in the SDK.
The guest kernel checks the permissions and sets the owner, just as it does for an exec. A write into the image’s /etc therefore fails for a user other than root. --user root can bypass ordinary file permissions, but not a read-only mount.
The rules
Section titled “The rules”- The sandbox must be running. A copy into or out of a stopped sandbox fails with
sandbox <name> is stopped: start it again with shard start <name>. A paused one fails withsandbox <name> is paused: resume it with shard resume <name>. - A write never leaves half a file. A copy in writes under a temporary name, syncs, then renames over the old file. If a copy is cut off midway, the old file stays intact.
- The daemon sets no size cap. Copies stream in both directions, and the daemon never holds the whole file. A directory copied out is the exception:
shard cpand the SDKs refuse one past 64 GiB of file data or 1,048,576 entries. - A file is only bytes. shard never puts a secret value into a file, even when the file holds a placeholder.
- A directory copied out is untrusted. The sandbox packs it, so
shard cpand the SDKs refuse an absolute name, a.., a hardlink to a name outside it and a device node. No symlink can lead outside the destination. They also drop setuid and setgid bits, and never change an owner on your machine. A refusal names the entry, and the copy writes nothing more. - A file call needs the
execscope. A write can replace what the sandbox runs, so a token that copies files can also run commands.
The image needs neither tar nor a shell for a copy, because shard’s own supervisor in the sandbox reads and writes the files. The REST API reference lists every file route.
To learn how a sandbox moves between states, see Sandboxes and the lifecycle.