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

# Use in Code

> Call Box from scripts and applications by wrapping the CLI with --json.

Use `--json` and parse stdout when automating the Box CLI. For typed HTTP clients, use the [Python SDK](/box/sdks/python) or [TypeScript/JavaScript SDK](/box/sdks/typescript).

Some commands emit one JSON object. Long-running commands emit JSON Lines: one JSON object per line.
Argument parsing errors can still be emitted by the CLI parser on stderr before Box's JSON error handler runs.

For non-interactive environments, create a Box API key with `box api-key create` and pass it as `BOX_API_KEY`; see [API Keys](/box/api-keys) for setup.

For runtime secrets inside Boxes, use [Dashboard > Secrets](https://box.ascii.dev/box/dashboard?tab=secrets). Dashboard secrets are injected as environment variables and secret files; configure them before running setup scripts or prompts that need credentials.

See [Secrets & Setup](/box/secrets) for the dashboard workflow and setup-script guidance.

<Warning>
  Treat `desktopUrl` and `viewerUrl` as secrets. They can include short-lived desktop access tokens.
</Warning>

## Authenticate

Authenticate once before the first command in a process or container:

```bash theme={null}
box login "$BOX_API_KEY" --json
```

Output:

```json theme={null}
{
  "event": "login_complete",
  "data": {
    "user": {
      "login": "octocat",
      "email": "octocat@example.com"
    }
  }
}
```

To provision the key itself per project, from a session that signed in through the browser flow (API-key auth cannot mint keys):

```bash theme={null}
BOX_API_KEY="$(box api-key create my-project --json | jq -r '.secret')"
```

The secret is only ever present in this one response; `box api-key list --json` returns metadata without secrets.

If you run `box login --json` without a key, the CLI starts the browser flow instead:

```json theme={null}
{
  "event": "login_url",
  "url": "https://ascii.dev/api/box/auth/github?state=...",
  "pollToken": "opaque_poll_token",
  "nextCommand": "box onboard --json",
  "instruction": "Ask the user to open this URL and sign in with GitHub. After the browser says it is complete, run nextCommand."
}
```

## Output contract

Commands that return a single JSON object:

| Command                            | Shape                |
| ---------------------------------- | -------------------- |
| `box config --json`                | `ConfigResult`       |
| `box list --json`                  | `BoxListResult`      |
| `box info <id> --json`             | `BoxInfoResult`      |
| `box limits --json`                | `LimitsResult`       |
| `box stop <id> --json`             | `BoxActionResult`    |
| `box resume <id> --json`           | `BoxActionResult`    |
| `box fork <id> --json`             | `BoxActionResult`    |
| `box interrupt <id> --json`        | `BoxActionResult`    |
| `box host <id> <port> --json`      | `HostResult`         |
| `box desktop <id> --json`          | `DesktopResult`      |
| `box desktop <id> --vnc --json`    | `VncDesktopResult`   |
| `box api-key create <name> --json` | `ApiKeySecretResult` |
| `box api-key rotate <id> --json`   | `ApiKeySecretResult` |
| `box api-key list --json`          | `ApiKeyListResult`   |
| `box api-key revoke <id> --json`   | `{ ok: true }`       |

Commands that emit JSONL:

| Command                                       | Lines                                                    |
| --------------------------------------------- | -------------------------------------------------------- |
| `box new --json`                              | `created`, zero or more `state`, then `ready` or `error` |
| `box prompt --provider codex <id> ... --json` | `queued`, then `chat` lines until the prompt finishes    |
| `box events <id> --json`                      | zero or more `chat` lines and then exits                 |
| `box events <id> --follow --json`             | `chat` lines until interrupted                           |

## Setup Scripts and Secrets

The usual setup flow is:

1. Configure secrets in [Dashboard > Secrets](https://box.ascii.dev/box/dashboard?tab=secrets).
2. Create a Box with `box new --json` and wait for the `ready` event.
3. Run a non-interactive command with `box ssh`.

Secrets configured in the dashboard are available as process environment variables and configured secret files inside the Box. Prefer this over passing secret values in prompts, URLs, or command arguments.

`box ssh <id> <command>` prepares and registers the CLI-managed SSH key, then runs the command without opening an interactive shell. It streams stdin, stdout, and stderr, so a setup script can run without copying it into the Box first.

```bash theme={null}
box_id="$(box new --json | jq -r 'select(.event == "ready") | .id')"
box ssh "$box_id" -- bash -s < ./setup.sh
box ssh "$box_id" "cd /home/user/ariana-ide-private && npm install"
box ssh "$box_id" -- bash -lc "cd /home/user/ariana-ide-private && npm test"
```

On Windows PowerShell:

```powershell theme={null}
$boxId = $null
box new --json | ForEach-Object {
  $event = $_ | ConvertFrom-Json
  if ($event.event -eq "ready") { $boxId = $event.id }
}
cmd /c "type setup.sh | box ssh $boxId -- bash -s"
box ssh $boxId "cd /home/user/ariana-ide-private && npm install"
```

PowerShell's native pipeline can keep stdin open for native executables in some environments. For Windows automation, run setup through Node, Python, WSL, Git Bash, or `cmd.exe`.

If you need the Box to keep running for a long uninterrupted workflow, disable auto-stop when creating it:

```bash theme={null}
box_id="$(box new --no-auto-stop --json | jq -r 'select(.event == "ready") | .id')"
```

See [Long-Running Tasks](/box/long-running-tasks).

Do not rely on runtime processes surviving resume or fork. After `box resume` or `box fork`, check app servers, workers, dev servers, tunnels, and desktop sessions, and run your setup or start command again if needed.

## Errors

Most failed CLI commands in `--json` mode emit one final JSON line to stdout and exit non-zero. Commands that normally emit one JSON object still use this JSONL error line on runtime failure. Argument parsing failures, such as missing required flags or invalid numeric flag values, are emitted by the CLI parser on stderr and may not be JSON.

```json theme={null}
{
  "event": "error",
  "error": "Human-readable error message"
}
```

Backend API failures include `code` and `status`. The backend field is named `error`; the CLI exposes that backend code as `code` so `error` can stay human-readable.

```json theme={null}
{
  "event": "error",
  "error": "not found (404). check the Box ID with `box list` (or use `current` if you created one in this shell)",
  "code": "not_found",
  "status": 404
}
```

Local CLI failures do not include `code` or `status` because no Box API error response was received:

```json theme={null}
{
  "event": "error",
  "error": "invalid provider \"nope\". Use codex or claude."
}
```

Network failures also use the local shape:

```json theme={null}
{
  "event": "error",
  "error": "could not reach the Box API at http://127.0.0.1:9: error sending request for url (...)"
}
```

Type:

```ts theme={null}
type CliErrorLine = {
  event: "error";
  error: string;
  code?: string;    // backend `error` code, present only for Box API failures
  status?: number;  // HTTP status, present only for Box API failures
}
```

The backend error object for Box CLI API paths is one of these shapes:

```ts theme={null}
type BoxApiError =
  | BasicBoxApiError
  | BlockedBoxApiError
  | RateLimitedBoxApiError
  | ProviderNotConfiguredError
  | BoxNotPromptableError
  | StopFailedError
  | SshKeyError;

type BasicBoxApiError = {
  error:
    | "unauthorized"
    | "forbidden"
    | "not_found"
    | "method_not_allowed"
    | "account_not_ready"
    | "prompt_required"
    | "provider_required"
    | "invalid_name"
    | "no_changes"
    | "resume_failed"
    | "fork_failed"
    | "machine_not_running"
    | "desktop_not_ready";
  message?: string;
}

type BoxLimitFields = {
  activeStates: ["provisioned", "cloning", "ready", "idle", "running"];
  activeBoxes: number;
  maxActiveBoxes: number;
  maxCreationRequestsPerMinute: number;
  maxCreationRequestsPerDay: number | null;
  currentLimits: {
    activeBoxes: number;
    creationRatePerMinute: number;
    creationRequestsPerDay: number | null;
  };
  standardLimits: {
    activeBoxes: number;
    creationRatePerMinute: number;
    creationRequestsPerDay: number | null;
  };
  trialLimits: {
    activeBoxes: number;
    creationRatePerMinute: number;
    creationRequestsPerDay: number | null;
  };
}

type BoxBillingFields = {
  hasSubscription: boolean;
  hasPaymentHistory: boolean;
  accountPlan: "user" | "service" | string;
  plan: "service" | string | null;
  planName: string | null;
  serviceAccount: boolean;
  unlimited: boolean;
  billingStatus: string;
  subscriptionStatus: string;
  subscriptionCancelAtPeriodEnd: boolean;
  subscriptionTrialEndsAt: string | null;
  subscriptionCurrentPeriodEnd: string | null;
  checkoutRequired: boolean;
  canStart: boolean;
  startBlockedReason: "subscription_required" | "usage_depleted" | string | null;
  blockedReason: "subscription_required" | "usage_depleted" | string | null;
  accessTier: "trial" | "standard" | "service" | string;
  creditBalanceSeconds: number;
  subscriptionQuotaSeconds: number;
  subscriptionRemainingSeconds: number;
  packBalanceSeconds: number;
  creditPurchasedSeconds: number;
  creditUsedSeconds: number;
  liveUsageSeconds: number;
  creditSecondsPerDollar: number;
  package: {
    dollars: number;
    seconds: number;
    secondsPerDollar: number;
  };
  upgradeEffects: {
    startTrial: {
      canCreate: true;
      activeBoxes: number;
      creationRequestsPerDay: number;
      note: string;
    };
    endTrialOrFirstPayment: {
      activeBoxes: number;
      creationRequestsPerDay: null;
      note: string;
    };
    pack: {
      seconds: number;
      persistsAcrossMonths: true;
      note: string;
    };
  };
  contactMessage: string;
}

type BlockedBoxApiError = (BoxLimitFields & BoxBillingFields) & {
  error: "billing_required" | "box_access_inactive" | "daily_limit_reached" | "limit_reached";
  status: "blocked";
  message: string;
  billingUrl: string;
}

type RateLimitedBoxApiError = {
  error: "rate_limited";
  status: "blocked";
  message: string;
  maxCreationRequestsPerMinute: number;
}

type ProviderNotConfiguredError = {
  error: "provider_not_configured";
  provider: "codex" | "claude-code";
  setupUrl: string;
  message: string;
}

type BoxNotPromptableError = {
  error: "box_not_promptable";
  state: string;
  message: string;
}

type StopFailedError = {
  error: "stop_failed";
  message: string;
  box: Box | null;
}

type SshKeyError = {
  success: false;
  error: "unauthorized" | "not_found" | "machine_not_running" | "invalid_ssh_key";
  message?: string;
}
```

The CLI uses `message` when present. If there is no `message`, it humanizes known backend codes such as `unauthorized`, `not_found`, `account_not_ready`, `method_not_allowed`, and `rate_limited`; otherwise it formats the backend code and HTTP status.

<Warning>
  Treat CLI error output as sensitive. For backend errors that include `billingUrl`, `setupUrl`, or `dashboardUrl`, the CLI may append a dashboard URL with `box_token` to the human-readable `error` string.
</Warning>

Common backend codes used by the Box CLI paths:

| Code                      | HTTP status | Where it comes from                                                                                                         |
| ------------------------- | ----------: | --------------------------------------------------------------------------------------------------------------------------- |
| `unauthorized`            |         401 | Missing, invalid, expired, or revoked Box auth.                                                                             |
| `billing_required`        |         402 | Creating, resuming, forking, or prompting is blocked by inactive plan or depleted usage.                                    |
| `box_access_inactive`     |         402 | Reading events or accessing an existing box is blocked by inactive plan or depleted time.                                   |
| `not_found`               |         404 | The Box ID or API key was not found for the current user.                                                                   |
| `account_not_ready`       |         409 | GitHub/Ascii account setup is incomplete.                                                                                   |
| `resume_failed`           |         409 | Backend could not resume the box.                                                                                           |
| `box_restoring`           |         409 | SSH/desktop access requested in the first seconds of a resume, before the filesystem is being served. Retry after a moment. |
| `fork_failed`             |         409 | Backend could not fork the box.                                                                                             |
| `provider_not_configured` |         409 | Prompting was requested before the selected provider was configured.                                                        |
| `box_not_promptable`      |         409 | Prompting was requested while the box is in an unsupported state.                                                           |
| `prompt_required`         |         400 | Prompt request body had no non-empty prompt.                                                                                |
| `provider_required`       |         400 | Prompt request body had no supported provider.                                                                              |
| `machine_not_running`     |         400 | Desktop or SSH key setup was requested before the machine was running.                                                      |
| `desktop_not_ready`       |         400 | Desktop streaming metadata was not ready.                                                                                   |
| `invalid_ssh_key`         |         400 | SSH key registration received a missing or invalid public key.                                                              |
| `invalid_name`            |         400 | Box rename received an empty name.                                                                                          |
| `no_changes`              |         400 | Box update request did not include a supported change.                                                                      |
| `stop_failed`             |         400 | Stop was requested while the box was in a state that cannot transition to stopping.                                         |
| `rate_limited`            |         429 | Too many box creation requests in one minute.                                                                               |
| `daily_limit_reached`     |         429 | Trial/pre-payment daily creation limit reached.                                                                             |
| `limit_reached`           |         429 | Active box limit reached.                                                                                                   |
| `method_not_allowed`      |         405 | API key management route received an unsupported method/action.                                                             |

Local CLI validation can fail before the backend is called. These errors have no stable `code` today. Examples from the current CLI source include invalid provider, invalid model, invalid reasoning effort, empty API key, missing local login, unsupported HTTP method, invalid local JSON from the API, SSH/SCP exit failures, and unreachable API host.

## Common types

### Box

```ts theme={null}
type Box = {
  id: string;                    // bx_...
  name: string;
  state: "init" | "provisioning" | "cloning" | "ready" | "idle" | "running" | "stopping" | "stopped" | "error" | string;
  url: string | null;            // public HTTPS URL for services
  ip: string | null;             // direct SSH IPv4 when available
  desktopAvailable: boolean;
  desktopUrl: string | null;     // secret-bearing desktop stream URL
  snapshotAvailable: boolean;
  snapshotCompletedAt: string | null;
  createdAt: string | null;      // ISO timestamp
  updatedAt: string | null;      // ISO timestamp
  archiveAfter: string | null;   // ISO timestamp, null means no automatic stop
  self?: boolean;                // only added by `box list --json`
}
```

The CLI presents backend states for users: `provisioned` becomes `ready`, `archiving` becomes `stopping`, and `archived` becomes `stopped`.

### BoxListResult

```ts theme={null}
type BoxListResult = {
  boxes: Box[];
  pageInfo?: {
    hasMore: boolean;
    limit: number;
    nextCursor: string | null;
  };
}
```

`box list --json` defaults to up/running boxes, the same as `box list --filter r --json`. Use `box list --filter s --json` for stopped boxes, combine groups like `--filter sr`, or use `--all --json` to include every state.

Example:

```json theme={null}
{
  "boxes": [
    {
      "id": "bx_8pqt6dup",
      "name": "Box 2026-05-28 16:00",
      "state": "idle",
      "url": "https://example-box.on.ascii.dev",
      "ip": "203.0.113.10",
      "desktopAvailable": true,
      "desktopUrl": "https://example-desktop.on.ascii.dev/stream.html?hostId=...&token=...",
      "snapshotAvailable": true,
      "snapshotCompletedAt": "2026-05-28T16:39:33.012Z",
      "createdAt": "2026-05-28T16:00:33.143Z",
      "updatedAt": "2026-05-28T16:00:39.535Z",
      "archiveAfter": "2026-05-28T17:00:33.162Z",
      "self": false
    }
  ]
}
```

### BoxInfoResult

```ts theme={null}
type BoxInfoResult = {
  box: Box;
}
```

### BoxActionResult

Used by `stop`, `resume`, `fork`, and `interrupt`.

```ts theme={null}
type BoxActionResult = {
  id: string;
  status: "stopping" | "resuming" | "forking" | string;
  box: Box | null;
}
```

### DesktopResult

```ts theme={null}
type DesktopResult = {
  success: true;
  desktopUrl: string;        // secret-bearing stream URL
  viewerUrl: string;         // secret-bearing dashboard viewer URL
  ip: string | null;
}
```

### HostResult

```ts theme={null}
type HostResult = {
  boxId: string;
  port: number;
  url: string;                // HTTPS URL for the exposed port
  access: "private" | "public";
  isProtected: boolean;       // true when url includes or requires _token
  title: string | null;
}
```

### VncDesktopResult

```ts theme={null}
type VncDesktopResult = {
  desktopUrl: string;        // secret-bearing noVNC URL
  mode: "vnc";
}
```

### ApiKeySecretResult

Returned by `box api-key create --json` and `box api-key rotate --json`. The `secret` exists only in this response and can never be retrieved again.

```ts theme={null}
type ApiKeyMetadata = {
  id: string;
  name: string;
  keyPrefix: string;           // first 12 characters
  keyLastFour: string;
  sandboxId: string | null;    // set on platform-managed per-box keys
  createdAt: string;           // ISO timestamp
  lastUsedAt: string | null;   // ISO timestamp
}

type ApiKeySecretResult = {
  apiKey: ApiKeyMetadata;
  secret: string;              // full key, shown once
}
```

### ApiKeyListResult

`box api-key list --json` returns keys you created; add `--all` to include platform-managed per-box machine keys (`sandboxId` set).

```ts theme={null}
type ApiKeyListResult = {
  apiKeys: ApiKeyMetadata[];
}
```

### ConfigResult

```ts theme={null}
type ConfigResult = {
  path: string;
  apiUrl: string | null;
  channel: string | null;
  loggedIn: boolean;
}
```

### LimitsResult

```ts theme={null}
type LimitsResult = {
  activeBoxes: number;
  maxActiveBoxes: number;
  maxCreationRequestsPerMinute: number;
  maxCreationRequestsPerDay: number | null;
  activeStates: string[];
  hasSubscription: boolean;
  hasPaymentHistory: boolean;
  accountPlan: "user" | "service" | string;
  plan: "service" | string | null;
  planName: string | null;
  serviceAccount: boolean;
  unlimited: boolean;
  billingStatus: string;
  subscriptionStatus: string;
  subscriptionCancelAtPeriodEnd: boolean;
  subscriptionTrialEndsAt: string | null;
  subscriptionCurrentPeriodEnd: string | null;
  checkoutRequired: boolean;
  canStart: boolean;
  startBlockedReason: "subscription_required" | "usage_depleted" | string | null;
  blockedReason: "subscription_required" | "usage_depleted" | string | null;
  accessTier: "trial" | "standard" | "service" | string;
  creditBalanceSeconds: number;
  subscriptionQuotaSeconds: number;
  subscriptionRemainingSeconds: number;
  packBalanceSeconds: number;
  creditPurchasedSeconds: number;
  creditUsedSeconds: number;
  liveUsageSeconds: number;
  creditSecondsPerDollar: number;
  package: {
    dollars: number;
    seconds: number;
    secondsPerDollar: number;
  };
  currentLimits: {
    activeBoxes: number;
    creationRatePerMinute: number;
    creationRequestsPerDay: number | null;
  };
  standardLimits: {
    activeBoxes: number;
    creationRatePerMinute: number;
    creationRequestsPerDay: number | null;
  };
  trialLimits: {
    activeBoxes: number;
    creationRatePerMinute: number;
    creationRequestsPerDay: number | null;
  };
  upgradeEffects: Record<string, unknown>;
  contactMessage: string;
}
```

## Create a box

`box new --json` emits JSONL, not a single JSON object.

```jsonl theme={null}
{"event":"created","id":"bx_8pqt6dup","ttlSeconds":3600}
{"event":"state","id":"bx_8pqt6dup","state":"provisioning"}
{"event":"state","id":"bx_8pqt6dup","state":"ready"}
{"event":"ready","id":"bx_8pqt6dup","state":"ready","url":"https://example-box.on.ascii.dev","ip":"203.0.113.10","desktopUrl":"https://example-desktop.on.ascii.dev/stream.html?hostId=...&token=...","archiveAfter":"2026-05-28T17:00:33.162Z","commands":{"ssh":"box ssh bx_8pqt6dup","forward":"box forward bx_8pqt6dup --remote 3000 --local 3000"}}
```

Schema:

```ts theme={null}
type NewCreatedLine = {
  event: "created";
  id: string;
  ttlSeconds: number | null;
}

type NewStateLine = {
  event: "state";
  id: string;
  state: Box["state"];
}

type NewReadyLine = {
  event: "ready";
  id: string;
  state: Box["state"];
  url: string | null;
  ip: string | null;
  desktopUrl: string | null;
  archiveAfter: string | null;
  commands: {
    ssh: string;
    forward: string;
  };
}
```

## Prompt and events

`box prompt --provider codex <id> "..." --json` first emits a queued line:

```json theme={null}
{
  "event": "queued",
  "data": {
    "id": "bx_chm52eme",
    "promptId": "09690a68-5aee-4b35-a2e7-eb1b64e3e104",
    "status": "queued",
    "provider": "codex",
    "model": null,
    "reasoningEffort": null
  }
}
```

Then it emits chat lines until the prompt finishes. `box events --json` emits the same chat line shape for persisted events. It may return fewer lines than `box prompt --json`; in a live check, `box prompt` emitted queued, queued-prompt, running-prompt, finished-prompt, and response lines, while `box events` later returned the persisted finished-prompt and response lines.

```json theme={null}
{
  "event": "chat",
  "final": true,
  "data": {
    "id": "a75c1426-23bf-4eae-9131-c957046a2af5",
    "taskId": "09690a68-5aee-4b35-a2e7-eb1b64e3e104",
    "type": "response",
    "timestamp": 1779986896114,
    "data": {
      "content": "box-jsonl-audit-ok",
      "is_reverted": false,
      "model": "gpt-5.4"
    }
  }
}
```

Schema:

```ts theme={null}
type QueuedLine = {
  event: "queued";
  data: {
    id: string;
    promptId: string;
    status: "queued";
    provider: "codex" | "claude-code" | string;
    model?: string | null;
    reasoningEffort?: string | null;
  };
}

type ChatLine = {
  event: "chat";
  final: boolean;          // false for streaming partial response events
  data: {
    id?: string;
    type: "prompt" | "response" | "usage_limit" | "git_checkpoint" | string;
    timestamp?: number;   // Unix epoch milliseconds
    taskId?: string;
    data: Record<string, unknown>;
  };
}
```

`response` chat events can include tool data in `data.tools`; keep the full object if you need exact agent traces.

## Bash JSONL parser

```bash theme={null}
set -euo pipefail

box login "$BOX_API_KEY" --json >/dev/null

box_id=""
while IFS= read -r line; do
  event="$(printf '%s\n' "$line" | jq -r '.event')"
  case "$event" in
    created)
      box_id="$(printf '%s\n' "$line" | jq -r '.id')"
      ;;
    ready)
      ip="$(printf '%s\n' "$line" | jq -r '.ip // empty')"
      echo "ready: $box_id $ip"
      ;;
    error)
      printf '%s\n' "$line" >&2
      exit 1
      ;;
  esac
done < <(box new --ttl 3600 --json)

box info "$box_id" --json
box stop "$box_id" --json
```

## Node.js

Use a single-object helper for commands like `info`, and a JSONL helper for commands like `new`.

```js theme={null}
import { spawnSync, spawn } from "node:child_process";
import { createInterface } from "node:readline";

class BoxCliError extends Error {
  constructor(line, fallback) {
    super(line?.error || fallback);
    this.name = "BoxCliError";
    this.code = line?.code;
    this.status = line?.status;
    this.line = line;
  }
}

function parseErrorLine(output) {
  const lastLine = output.trim().split(/\r?\n/).filter(Boolean).at(-1);
  if (!lastLine) return null;
  try {
    const parsed = JSON.parse(lastLine);
    return parsed.event === "error" ? parsed : null;
  } catch {
    return null;
  }
}

function boxJson(args) {
  const result = spawnSync("box", [...args, "--json"], {
    encoding: "utf8",
    stdio: ["ignore", "pipe", "pipe"],
  });

  if (result.status !== 0) {
    throw new BoxCliError(
      parseErrorLine(result.stdout),
      result.stderr || `box ${args.join(" ")} failed`
    );
  }

  return JSON.parse(result.stdout);
}

async function* boxJsonLines(args) {
  const child = spawn("box", [...args, "--json"], {
    stdio: ["ignore", "pipe", "pipe"],
  });
  const rl = createInterface({ input: child.stdout });
  let lastError = null;

  for await (const line of rl) {
    if (!line.trim()) continue;
    const parsed = JSON.parse(line);
    if (parsed.event === "error") lastError = parsed;
    yield parsed;
  }

  const exitCode = await new Promise((resolve) => child.on("close", resolve));
  if (exitCode !== 0) {
    throw new BoxCliError(lastError, `box ${args.join(" ")} exited with ${exitCode}`);
  }
}

boxJson(["login", process.env.BOX_API_KEY]);

let boxId;
for await (const line of boxJsonLines(["new", "--ttl", "3600"])) {
  if (line.event === "created") boxId = line.id;
  if (line.event === "ready") console.log("ready", line);
}

console.log(boxJson(["info", boxId]));
boxJson(["stop", boxId]);
```

## Python

```python theme={null}
import json
import os
import subprocess


class BoxCliError(Exception):
    def __init__(self, line, fallback):
        super().__init__((line or {}).get("error") or fallback)
        self.line = line
        self.code = (line or {}).get("code")
        self.status = (line or {}).get("status")


def parse_error_line(output):
    lines = [line for line in output.splitlines() if line.strip()]
    if not lines:
        return None
    try:
        parsed = json.loads(lines[-1])
    except json.JSONDecodeError:
        return None
    return parsed if parsed.get("event") == "error" else None


def box_json(*args):
    result = subprocess.run(
        ["box", *args, "--json"],
        text=True,
        capture_output=True,
    )
    if result.returncode != 0:
        raise BoxCliError(
            parse_error_line(result.stdout),
            result.stderr or f"box {' '.join(args)} failed",
        )
    return json.loads(result.stdout)


def box_json_lines(*args):
    process = subprocess.Popen(
        ["box", *args, "--json"],
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    assert process.stdout is not None
    last_error = None
    for line in process.stdout:
        if line.strip():
            parsed = json.loads(line)
            if parsed.get("event") == "error":
                last_error = parsed
            yield parsed
    exit_code = process.wait()
    if exit_code != 0:
        stderr = process.stderr.read() if process.stderr else ""
        raise BoxCliError(last_error, stderr or f"box {' '.join(args)} exited with {exit_code}")


box_json("login", os.environ["BOX_API_KEY"])

box_id = None
for event in box_json_lines("new", "--ttl", "3600"):
    if event["event"] == "created":
        box_id = event["id"]
    if event["event"] == "ready":
        print("ready", event)

print(box_json("info", box_id))
box_json("stop", box_id)
```

## Direct SSH

The CLI manages the SSH key. For an interactive shell, prefer:

```bash theme={null}
box ssh "$box_id"
box scp ./local-file.txt "$box_id:/home/user/local-file.txt"
```

For `box scp`, remote paths are passed through to OpenSSH `scp` after the `<box-id>:` prefix. Absolute paths such as `/home/user/setup.sh` are the safest. Relative remote paths are resolved from the SSH user's home directory (`/home/user` on hosted Boxes). Avoid relying on `~` in automation because expansion can vary by local shell and `scp` mode.

For non-interactive commands, pass the command after the box ID:

```bash theme={null}
box ssh "$box_id" "cd /home/user/ariana-ide-private && npm test"
box ssh "$box_id" -- bash -lc "cd /home/user/ariana-ide-private && npm test"
box ssh "$box_id" -- bash -s < ./setup.sh
```

For direct SSH, inspect the Box for its IP and use the CLI-managed key:

```bash theme={null}
ip="$(box info "$box_id" --json | jq -r '.box.ip // empty')"
ssh -i ~/.ssh/ascii_box_ed25519 "user@$ip"
```

## Related

* [API Keys](/box/api-keys)
* [Secrets & Setup](/box/secrets)
* [Long-Running Tasks](/box/long-running-tasks)
* [SSH Access](/box/ssh-access)
