> ## Documentation Index
> Fetch the complete documentation index at: https://boxrouter.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Work with Files

> Write a file into a sandbox, run a command against it, and read the result back.

A sandbox has its own disk. You write input into it, run a command, and read the output back
before the sandbox ends. Files you need after that belong in a [Volume](https://boxrouter.ai/docs/volumes.md).

**Before You Begin:** `BOXROUTER_API_KEY` set and a client installed, as in the [quickstart](https://boxrouter.ai/docs/quickstart.md).

## Pick a Call by Size

| File | Use | What Happens |
| - | - | - |
| Up to 1 MiB | `write` and `read` | One request holds the whole file in memory. |
| Over 1 MiB | [Volumes](https://boxrouter.ai/docs/volumes.md) | Store larger files in a Volume, then attach it to a sandbox. |

Each sandbox `read` or `write` call holds the whole file in memory up to `limits.file_transfer_bytes`.
A larger `read` fails with `413 output_limit`, and a larger `write` fails with `413 resource_limit`.
Use a Volume for larger files.

## Write, Run, Read

This writes `hello`, upper-cases it with a command in the sandbox, and reads the result. Bytes go
in and come out exactly: the SDKs use `bytes` in Python and `Uint8Array` in TypeScript, and
decode only text. The HTTP example uses `curl` and `jq`.

Each example requests sandbox cleanup. [Recover from Errors](https://boxrouter.ai/docs/errors.md#when-cleanup-fails) covers a deletion
that stays pending.

```python Python theme={"theme":"css-variables"}
from boxrouter import BoxRouter

with BoxRouter() as client:
    with client.sandboxes.create() as box:
        box.files.write("/tmp/input.txt", b"hello\n")
        box.exec("tr a-z A-Z < /tmp/input.txt > /tmp/output.txt", check=True)
        print(box.files.read("/tmp/output.txt").decode(), end="")
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

await using client = new BoxRouter();
await using box = await client.sandboxes.create();
await box.files.write('/tmp/input.txt', new TextEncoder().encode('hello\n'));
await box.exec('tr a-z A-Z < /tmp/input.txt > /tmp/output.txt', { check: true });
process.stdout.write(await box.files.read('/tmp/output.txt'));
```

```bash CLI theme={"theme":"css-variables"}
(
  set -euo pipefail
  id=$(boxrouter create)
  trap 'boxrouter destroy "$id"' EXIT
  printf 'hello\n' | boxrouter write "$id" --path /tmp/input.txt --body -
  boxrouter exec "$id" 'tr a-z A-Z < /tmp/input.txt > /tmp/output.txt'
  boxrouter read "$id" --path /tmp/output.txt
)
```

```bash HTTP theme={"theme":"css-variables"}
(
  set -euo pipefail
  API="${BOXROUTER_BASE_URL:-https://api.boxrouter.ai/api/v1}"
  AUTH="Authorization: Bearer $BOXROUTER_API_KEY"

  id=$(curl -sSf -X POST "$API/sandboxes" -H "$AUTH" | jq -er .id)
  trap 'curl -sSf -X DELETE "$API/sandboxes/$id" -H "$AUTH" >/dev/null' EXIT
  printf 'hello\n' | curl -sSf -X PUT "$API/sandboxes/$id/files/content?path=/tmp/input.txt" \
    -H "$AUTH" -H "Content-Type: application/octet-stream" --data-binary @-
  curl -sSf "$API/sandboxes/$id/commands" -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"command": "tr a-z A-Z < /tmp/input.txt > /tmp/output.txt"}' | jq -e '.exit_code == 0' >/dev/null
  curl -sSf "$API/sandboxes/$id/files/content?path=/tmp/output.txt" -H "$AUTH"
)
```

Output

```text theme={"theme":"css-variables"}
HELLO
```

`write` creates any missing parent directories, and the file is readable and writable by the user
that commands run as. It promises nothing more: BoxRouter sets no specific owner or permissions,
adds no executable bit, and does not make replacing a file atomic.

Reading a path that doesn't exist fails with `404 not_found` and `outcome: not_started`.

## Store Large Files

Use a [Volume](https://boxrouter.ai/docs/volumes.md) for a file that exceeds the inline limit or needs to outlive a sandbox.
Upload the file to the Volume, then attach it to the sandbox that uses it.

## Manage Directories

`list` reads the immediate entries of a directory without recursing or following links. The API
describes entries sorted by name, each with a `type` of `file`, `directory`, `symlink` or `other`.

`exists` checks a path. `remove` needs `recursive` for a directory that has contents; without it
the call fails with `invalid_state`. Each example creates and then requests deletion of a sandbox.

```python Python theme={"theme":"css-variables"}
from boxrouter import BoxRouter

with BoxRouter() as client:
    with client.sandboxes.create() as box:
        box.files.mkdir("/tmp/job/results", parents=True)
        box.files.write("/tmp/job/notes.txt", b"hi")
        for entry in box.files.list("/tmp/job"):
            print(entry.name)
        box.files.remove("/tmp/job", recursive=True)
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

await using client = new BoxRouter();
await using box = await client.sandboxes.create();
await box.files.mkdir('/tmp/job/results', { parents: true });
await box.files.write('/tmp/job/notes.txt', new TextEncoder().encode('hi'));
for (const entry of await box.files.list('/tmp/job')) console.log(entry.name);
await box.files.remove('/tmp/job', { recursive: true });
```

```bash CLI theme={"theme":"css-variables"}
(
  set -euo pipefail
  id=$(boxrouter create)
  trap 'boxrouter destroy "$id" >/dev/null' EXIT
  boxrouter api files mkdir "$id" --body '{"path": "/tmp/job/results", "parents": true}' >/dev/null
  printf 'hi' | boxrouter write "$id" --path /tmp/job/notes.txt --body -
  boxrouter api files list "$id" --path /tmp/job | jq -r '.[].name'
  boxrouter api files remove "$id" --body '{"path": "/tmp/job", "recursive": true}' >/dev/null
)
```

```bash HTTP theme={"theme":"css-variables"}
(
  set -euo pipefail
  API="${BOXROUTER_BASE_URL:-https://api.boxrouter.ai/api/v1}"
  AUTH="Authorization: Bearer $BOXROUTER_API_KEY"
  sandbox=$(curl -sSf -X POST "$API/sandboxes" -H "$AUTH" | jq -er .id)
  trap 'curl -sSf -X DELETE "$API/sandboxes/$sandbox" -H "$AUTH" >/dev/null' EXIT
  curl -sSf -X POST "$API/sandboxes/$sandbox/files/directories" -H "$AUTH" \
    -H 'Content-Type: application/json' -d '{"path":"/tmp/job/results","parents":true}' >/dev/null
  printf 'hi' | curl -sSf -X PUT "$API/sandboxes/$sandbox/files/content?path=/tmp/job/notes.txt" \
    -H "$AUTH" -H 'Content-Type: application/octet-stream' --data-binary @-
  curl -sSf "$API/sandboxes/$sandbox/files/directories?path=/tmp/job" -H "$AUTH" | jq -r '.[].name'
)
```

Output

```text theme={"theme":"css-variables"}
notes.txt
results
```

A directory with more entries than the route allows fails to list; it is never cut short. List
part of it from a command, such as `ls | head -n 1000`.

## Recover from a Failed Write

A failed `write` or `remove` can leave partial changes. If the error's `outcome` is `unknown`,
[read the operation](https://boxrouter.ai/docs/errors.md#inspect-an-uncertain-operation) before you retry.

Send an idempotency key with the original request, and reuse it for the same request if a retry is safe.

Each operation's guarantees are in the reference:

* [Read a file](https://boxrouter.ai/docs/reference/read.md) and [Write a file](https://boxrouter.ai/docs/reference/write.md)
* [List a directory](https://boxrouter.ai/docs/reference/list-directory.md) and [Create a directory](https://boxrouter.ai/docs/reference/create-directory.md)
* [Check whether a path exists](https://boxrouter.ai/docs/reference/path-exists.md) and [Remove a path](https://boxrouter.ai/docs/reference/remove-path.md)

## Next Steps

> [card] [Keep Files across Sandboxes](https://boxrouter.ai/docs/volumes.md)
>
> Keep files after the sandbox is destroyed.

> [card] [Run a Background Process](https://boxrouter.ai/docs/processes.md)
>
> Produce output with a long-running command.

> [card] [Recover from Errors](https://boxrouter.ai/docs/errors.md)
>
> Recover from a failed write.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.