# Create a sandbox, run commands and move files

> Create a sandbox from an image, run commands in it, and copy files between your machine and the sandbox.

Source: https://useshards.com/docs/essentials/run-commands/

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](https://useshards.com/docs/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](https://useshards.com/docs/quickstart/client/).

**CLI**

```sh
# 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.py
shard cp main.py agent:/srv/main.py
shard 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.txt
cat out.txt

# List your sandboxes, then remove this one.
shard list
shard remove --force agent
```

**TypeScript**

```ts
import { 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();
}
```

**Python**

```python
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

`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

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.

**CLI**

```sh
# Write the file on your machine, then copy it in.
printf '{"n": 3}\n' > input.json
shard cp input.json agent:/srv/input.json
```

**TypeScript**

```ts
// 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" });
```

**Python**

```python
# 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

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

- **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 with `sandbox <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 cp` and 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 cp` and 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 `exec` scope.** 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](https://useshards.com/docs/reference/api/files/) lists every file route.

To learn how a sandbox moves between states, see [Sandboxes and the lifecycle](https://useshards.com/docs/concepts/lifecycle/).
