> ## 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.

# Run Commands

> Run a shell command in a sandbox, read its output and exit code, and know what happens when it runs too long.

`exec` runs one shell command in a sandbox and returns its stdout, stderr, and exit code in a
single response. Use it for commands that finish within the 60-second default `timeout_seconds` and
print under 512 KiB per stream.

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

## Choose How to Run It

| You Need | Use | Because |
| - | - | - |
| A result within `timeout_seconds`, at most 512 KiB per stream | `exec` | One call returns everything. |
| A build, a long job or a server, or output larger than one `exec` result | [A background process](https://boxrouter.ai/docs/processes.md) | It outlives your client, and you read its output as it grows. |
| A shell a person types into | [A terminal](https://boxrouter.ai/docs/cli.md#open-a-terminal) | It is a real PTY, so programs that need a terminal work. |

## Run It and Read the Result

`exec` returns `stdout`, `stderr`, and `exit_code`. This example runs `echo hello` and prints the
output and the exit code. The command runs once, as Bash source, in a login shell.

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

with BoxRouter() as client:
    with client.sandboxes.create() as box:
        result = box.exec("echo hello")
        print(result.stdout, end="")
        print("exit code:", result.exit_code)
```

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

await using client = new BoxRouter();
await using box = await client.sandboxes.create();
const result = await box.exec('echo hello');
process.stdout.write(result.stdout);
console.log('exit code:', result.exit_code);
```

```bash CLI theme={"theme":"css-variables"}
boxrouter run 'echo hello'
echo "exit code: $?"
```

```bash HTTP theme={"theme":"css-variables"}
(
  set -eo 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 -r .id)
  trap 'curl -sSf -o /dev/null -X DELETE "$API/sandboxes/$id" -H "$AUTH"' EXIT
  curl -sSf "$API/sandboxes/$id/commands" -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"command": "echo hello"}' | jq -j '.stdout, "exit code: \(.exit_code)\n"'
)
```

Output

```text theme={"theme":"css-variables"}
hello
exit code: 0
```

The HTTP example needs curl 7.76 or later and `jq`. Leaving the Python `with` block, the TypeScript
`finally`, or the HTTP `trap` requests deletion; it does not confirm it.
[Recover from Errors](https://boxrouter.ai/docs/errors.md#when-cleanup-fails) covers a deletion that stays pending.

## A Nonzero Exit Is a Result

A command that exits nonzero is a completed result, not an error. The call succeeds and `exit_code`
carries the status. This command writes to stderr and exits with 3.

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

with BoxRouter() as client:
    with client.sandboxes.create() as box:
        result = box.exec('echo "disk full" >&2; exit 3')
        print(result.stderr, end="")
        print("exit code:", result.exit_code)
```

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

await using client = new BoxRouter();
await using box = await client.sandboxes.create();
const result = await box.exec('echo "disk full" >&2; exit 3');
process.stdout.write(result.stderr);
console.log('exit code:', result.exit_code);
```

```bash CLI theme={"theme":"css-variables"}
boxrouter run 'echo "disk full" >&2; exit 3'
echo "exit code: $?"
```

```bash HTTP theme={"theme":"css-variables"}
(
  set -eo 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 -r .id)
  trap 'curl -sSf -o /dev/null -X DELETE "$API/sandboxes/$id" -H "$AUTH"' EXIT
  curl -sSf "$API/sandboxes/$id/commands" -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"command": "echo \"disk full\" >&2; exit 3"}' | jq -j '.stderr, "exit code: \(.exit_code)\n"'
)
```

Output

```text theme={"theme":"css-variables"}
disk full
exit code: 3
```

A nonzero CLI status can also mean a client or API error, so read stderr before you treat it as the
command's exit status. A failed request raises `APIError` in the SDKs. To decide whether a failed call ran, read
the error's `outcome` ([Recover from Errors](https://boxrouter.ai/docs/errors.md)).
To raise on a nonzero exit instead, see [Stop on Failure](#stop-on-failure).

## Options

| Option | Default | What It Does |
| - | - | - |
| `cwd` | The sandbox's default directory | An absolute directory that exists in the sandbox. |
| `env` | Nothing added | Variables for this command only. |
| `timeout_seconds` | 60 | The deadline for the command. |
| `output` | `text` | `bytes` returns the exact bytes, which text mode would alter if they are not valid UTF-8. |
| `output_limit_bytes` | 524288 | The most bytes returned per stream, and the maximum you can ask for. |

This example sets `cwd` and `env` for one command.

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

with BoxRouter() as client:
    with client.sandboxes.create() as box:
        result = box.exec('echo "$APP_ENV in $PWD"', cwd="/tmp", env={"APP_ENV": "test"})
        print(result.stdout, end="")
```

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

await using client = new BoxRouter();
await using box = await client.sandboxes.create();
const result = await box.exec('echo "$APP_ENV in $PWD"', { cwd: '/tmp', env: { APP_ENV: 'test' } });
process.stdout.write(result.stdout);
```

```bash CLI theme={"theme":"css-variables"}
boxrouter run --cwd /tmp --env '{"APP_ENV": "test"}' 'echo "$APP_ENV in $PWD"'
```

```bash HTTP theme={"theme":"css-variables"}
(
  set -eo 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 -r .id)
  trap 'curl -sSf -o /dev/null -X DELETE "$API/sandboxes/$id" -H "$AUTH"' EXIT
  curl -sSf "$API/sandboxes/$id/commands" -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"command": "echo \"$APP_ENV in $PWD\"", "cwd": "/tmp", "env": {"APP_ENV": "test"}}' | jq -j .stdout
)
```

Output

```text theme={"theme":"css-variables"}
test in /tmp
```

Every result carries `stdout_truncated` and `stderr_truncated`. Output over the limit is not an error: the
result holds the first `output_limit_bytes` of that stream, and the matching flag is `true`. To get all of the
output, run the command as a [process](https://boxrouter.ai/docs/processes.md).

A request that sets `output_limit_bytes` above 524288 is rejected with `outcome: not_started`, so the command
did not run. A `413 output_limit` error with `outcome: unknown` means the output was too large to deliver. The
command may have run, so check its effects before you retry.

## Stop on Failure

Pass `check=True` (`{ check: true }` in TypeScript) to raise `CommandFailed` when the command exits
nonzero or times out. The error carries `exit_code`, `stdout`, `stderr`, and `timed_out`.

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

with BoxRouter() as client:
    with client.sandboxes.create() as box:
        try:
            box.exec('echo "disk full" >&2; exit 3', check=True)
        except CommandFailed as failure:
            print(failure.stderr, end="")
            print("exit code:", failure.exit_code)
```

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

await using client = new BoxRouter();
await using box = await client.sandboxes.create();
try {
  await box.exec('echo "disk full" >&2; exit 3', { check: true });
} catch (error) {
  if (!(error instanceof CommandFailed)) throw error;
  process.stdout.write(error.stderr as string);
  console.log('exit code:', error.exit_code);
}
```

```bash CLI theme={"theme":"css-variables"}
boxrouter run 'echo "disk full" >&2; exit 3' || echo "exit code: $?"
```

```bash HTTP theme={"theme":"css-variables"}
(
  set -eo 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 -r .id)
  trap 'curl -sSf -o /dev/null -X DELETE "$API/sandboxes/$id" -H "$AUTH"' EXIT
  curl -sSf "$API/sandboxes/$id/commands" -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"command": "echo \"disk full\" >&2; exit 3"}' \
    | jq -j 'select(.exit_code != 0 or .timed_out) | .stderr, "exit code: \(.exit_code)\n"'
)
```

Output

```text theme={"theme":"css-variables"}
disk full
exit code: 3
```

Without `check`, the same call returns the result and raises nothing.

## When a Command Runs Too Long

When `timeout_seconds` runs out, BoxRouter sends `TERM` to the command's process group, then `KILL` 5 seconds
later. The call still succeeds: the result has `timed_out` set to `true`, `exit_code` set to `null`, and the output
the command wrote before it was ended. This command prints one line, then sleeps past its 3-second deadline.

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

with BoxRouter() as client:
    with client.sandboxes.create() as box:
        result = box.exec("echo early; sleep 30; echo late", timeout_seconds=3)
        print(result.stdout, end="")
        print("timed out" if result.timed_out else "finished")
```

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

await using client = new BoxRouter();
await using box = await client.sandboxes.create();
const result = await box.exec('echo early; sleep 30; echo late', { timeout_seconds: 3 });
process.stdout.write(result.stdout);
console.log(result.timed_out ? 'timed out' : 'finished');
```

```bash CLI theme={"theme":"css-variables"}
boxrouter run --timeout-seconds 3 'echo early; sleep 30; echo late'
[ $? -eq 124 ] && echo "timed out"
```

```bash HTTP theme={"theme":"css-variables"}
(
  set -eo 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 -r .id)
  trap 'curl -sSf -o /dev/null -X DELETE "$API/sandboxes/$id" -H "$AUTH"' EXIT
  curl -sSf "$API/sandboxes/$id/commands" -H "$AUTH" -H "Content-Type: application/json" \
    -d '{"command": "echo early; sleep 30; echo late", "timeout_seconds": 3}' \
    | jq -j '.stdout, if .timed_out then "timed out\n" else "finished\n" end'
)
```

Output

```text theme={"theme":"css-variables"}
early
timed out
```

The CLI exits with status 124 and prints `Command timed out; the output it wrote is above.` on stderr.
The sandbox keeps running. A child that left the group, such as a daemon started with `setsid`, is not ended.

A `504 timeout` with `outcome: unknown` is rare. It means BoxRouter lost track of the command and cannot say whether
it ran, so check its effects before you resend it. [Lifecycle](https://boxrouter.ai/docs/lifecycle.md#the-four-clocks) compares the timeout with the other clocks.

A command's stdin is empty. [Run a command](https://boxrouter.ai/docs/reference/exec.md) lists every field and the
guarantees on each route.

## Custom Image Requirements

To enforce the deadline and capture the streams, BoxRouter wraps each command using shell tools.
A custom image must contain them. [What the image must contain](https://boxrouter.ai/docs/images.md#what-the-image-must-contain)
lists them.

Image preparation does not check for these tools, so the failure appears when you run a command:
`422 unsupported_feature`, with `details.prerequisite` naming the missing tool when known.
`outcome` is always `not_started`: the command never began. Rebuild the image, then
run the command again.

## Next Steps

> [card] [Run a Background Process](https://boxrouter.ai/docs/processes.md)
>
> Run long jobs and servers, and read output as it grows.

> [card] [Work with Files](https://boxrouter.ai/docs/files.md)
>
> Move files in and out of a sandbox.

> [card] [Recover from Errors](https://boxrouter.ai/docs/errors.md)
>
> Tell whether a failed call ran before you resend it.


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