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

# Desktop Streaming

> How Box opens a browser-based desktop stream for GUI work, and how its agents drive the same screen.

Every box includes a browser-accessible Linux desktop. Use it when you need to inspect a running app visually, use Chrome, debug Electron apps, or control GUI tools that are awkward over SSH. The box's own agents drive that same screen through the built-in [`computer` tools](/box/integrated-agents#computer-use), so you can watch a prompt do GUI work live.

## Open the desktop

From your local machine, open an authenticated desktop URL. Integrations can request the same URL through the SDK or API:

<CodeGroup>
  ```bash CLI theme={null}
  box desktop bx_f7k2q9hd
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/desktop?theme=light" \
    -H "Authorization: Bearer $BOX_API_KEY"
  ```

  ```ts TypeScript theme={null}
  const desktop = await box.desktop({ boxId: "bx_f7k2q9hd", theme: "light" });
  console.log(desktop.desktopUrl);
  ```

  ```python Python theme={null}
  desktop = box.desktop("bx_f7k2q9hd", theme="light")
  print(desktop.desktop_url)
  ```
</CodeGroup>

You can also open the desktop from the [Boxes](https://box.ascii.dev/box/dashboard?tab=boxes) tab of the dashboard while the box is running.

<Note>
  Desktop streaming is available only after the box's machine is up. If the box is provisioning, archived, or stopped, start or resume it first.
</Note>

## Streaming modes

The default desktop streams video and audio over **Moonlight** (WebRTC) at 1920x1080 and 60 fps. WebRTC relies on UDP and peer connectivity, so on restrictive, corporate, or low-bandwidth networks it can be choppy or fail to connect.

For those cases, use **VNC**, which tunnels over plain HTTPS and is far more tolerant of poor connections (at a lower frame rate):

<CodeGroup>
  ```bash CLI theme={null}
  box desktop bx_f7k2q9hd --vnc
  box desktop bx_f7k2q9hd --vnc --public
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/desktop?vnc=1" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"publicAccess":true}'
  ```

  ```ts TypeScript theme={null}
  const vnc = await box.desktop({
    boxId: "bx_f7k2q9hd",
    vnc: 1,
    requestBody: { publicAccess: true },
  });
  console.log(vnc.desktopUrl);
  ```

  ```python Python theme={null}
  vnc = box.desktop("bx_f7k2q9hd", vnc=1, request_body={"publicAccess": True})
  print(vnc.desktop_url)
  ```
</CodeGroup>

You don't have to decide up front: the browser viewer shows a **"Switch to VNC"** option while the default stream is connecting or playing, and again if it errors. The dashboard box menu exposes **Desktop**, **Browser**, and **Desktop via VNC**.

<Note>
  The VNC viewer opens as its own top-level page (not embedded), which is required for its connection to authenticate. The first `--vnc` on a box takes a few seconds to prepare; later opens are instant.
</Note>

## Open only the browser

Use a browser-only view when someone should see and control Chrome without seeing the Box desktop. From the dashboard, open the box menu and choose **Browser**. From the CLI:

```bash theme={null}
box browser bx_f7k2q9hd
box browser bx_f7k2q9hd --profile /data/chrome-profile
```

Pass `--profile <absolute path on the box>` to run the stream on a specific Chrome user data dir, so one box can hold several signed-in profiles and you choose which one to open. The directory is created for you, must sit outside system directories and outside `/home/user`, and travels with the box's snapshots and forks. Boxes created before this feature return `chrome_profile_unsupported`.

This opens a separate browser surface on the same running Box. The stream is 1920x1080 at 60 fps, the same as the default desktop. Chrome keeps its tabs and address bar. Window-close chrome is hidden on purpose, and Chrome's minimize cannot leave the stream on a blank desktop. Closing the last tab opens a new tab and does not tear down the stream. Running `box browser` again leaves the live stream up and prints a new URL. Opening that URL in a second tab takes the stream from the first tab. Desktop and browser can run together. Two browser viewer tabs cannot share. The new one takes the stream. The viewer keeps the same mute, reconnect, and fullscreen controls as the desktop stream. New boxes get the browser-view files at provision; the guest image must already have Chrome, X, Openbox, Sunshine, and Moonlight. Its Moonlight process, streaming identity, active-stream slot, and clipboard policy are isolated from the desktop stream, so browser URLs cannot attach to the direct desktop stream. It does not create another Box, and it never falls back to the desktop or VNC.

<Warning>
  Browser-only means the stream is confined to Chrome and does not expose controls for closing it. It is not a security boundary against code already running inside the Box. A Box user with arbitrary code execution or root can inspect or change the browser process and its data. Treat the returned URL as a secret.
</Warning>

## Drive the desktop

The desktop on this page is the same screen the Box's coding agents drive. Every harness on a Box has a built-in set of `computer` tools for screenshots, clicks, typing, scrolling, and launching apps, so a prompt is all it takes:

<CodeGroup>
  ```bash CLI theme={null}
  box prompt bx_f7k2q9hd "Open example.com in Chrome and tell me the page title"
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/prompt" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"prompt":"Open example.com in Chrome and tell me the page title"}'
  ```

  ```ts TypeScript theme={null}
  await box.prompt({
    boxId: "bx_f7k2q9hd",
    promptRequest: { prompt: "Open example.com in Chrome and tell me the page title" },
  });
  ```

  ```python Python theme={null}
  box.prompt("bx_f7k2q9hd", PromptRequest(
      prompt="Open example.com in Chrome and tell me the page title",
  ))
  ```
</CodeGroup>

Open a stream in one window and watch it happen in the other. There is nothing to install and nothing to register: the tools are already on every harness. See [Computer use](/box/integrated-agents#computer-use) for the full tool list, the per-harness names, and the health check.

Shell commands still work on the same screen, and mix freely with prompts. Put a page up yourself, then hand the box a visual task:

<CodeGroup>
  ```bash CLI theme={null}
  box exec bx_f7k2q9hd 'DISPLAY=:0 google-chrome-stable "https://example.com" & sleep 2; wmctrl -a Chrome'
  box prompt bx_f7k2q9hd "Complete the checkout flow in the open browser window"
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"DISPLAY=:0 google-chrome-stable \"https://example.com\" & sleep 2; wmctrl -a Chrome"}'
  ```

  ```ts TypeScript theme={null}
  await box.command({
    boxId: "bx_f7k2q9hd",
    commandRequest: { command: 'DISPLAY=:0 google-chrome-stable "https://example.com" & sleep 2; wmctrl -a Chrome' },
  });
  ```

  ```python Python theme={null}
  box.command("bx_f7k2q9hd", CommandRequest(
      command='DISPLAY=:0 google-chrome-stable "https://example.com" & sleep 2; wmctrl -a Chrome',
  ))
  ```
</CodeGroup>

A Box has one screen, and every conversation on it drives that same screen. Two agents clicking at once will fight over it, so give each parallel GUI task its own Box, or run them one after another.

## Record what happens on screen

`ascii-record-desktop` captures the live desktop to an MP4 from inside the Box, which is the simplest way to produce a demo or keep evidence of a GUI run:

<CodeGroup>
  ```bash CLI theme={null}
  box exec bx_f7k2q9hd "ascii-record-desktop start /home/user/demo.mp4"
  # ... drive the desktop ...
  box exec bx_f7k2q9hd "ascii-record-desktop stop"
  box scp bx_f7k2q9hd:/home/user/demo.mp4 ./demo.mp4
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOX_API_BASE/boxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"ascii-record-desktop start /home/user/demo.mp4"}'

  curl -L -o demo.mp4 "$BOX_API_BASE/boxes/bx_f7k2q9hd/artifacts?path=demo.mp4" \
    -H "Authorization: Bearer $BOX_API_KEY"
  ```

  ```ts TypeScript theme={null}
  await box.command({
    boxId: "bx_f7k2q9hd",
    commandRequest: { command: "ascii-record-desktop start /home/user/demo.mp4" },
  });
  const video = await box.artifact({ boxId: "bx_f7k2q9hd", path: "demo.mp4" });
  ```

  ```python Python theme={null}
  box.command("bx_f7k2q9hd", CommandRequest(
      command="ascii-record-desktop start /home/user/demo.mp4",
  ))
  video = box.artifact("bx_f7k2q9hd", "demo.mp4")
  ```
</CodeGroup>

`ascii-record-desktop status` reports whether a capture is running. To share a recording instead of downloading it, serve its folder from the box and expose it with a token-gated URL:

```bash theme={null}
box ssh bx_f7k2q9hd "cd ~ && (python3 -m http.server 8090 --bind 0.0.0.0 &) && host 8090 --private"
```

## What happens when you open it

The CLI or dashboard asks Box for a fresh authenticated desktop URL for your box. If the desktop stream is not ready yet, Box prepares it and then opens the browser viewer.

The returned URL opens a browser page. For the default Moonlight stream it looks like this:

```text theme={null}
https://<box-desktop-host>/stream.html?hostId=<host>&appId=<app>&theme=light#token=<token>
```

For a VNC stream (`--vnc`) it points at a noVNC page instead:

```text theme={null}
https://<box-vnc-host>/vnc.html?autoconnect=true&password=<pw>&_token=<token>
```

If you request `--public` or send `publicAccess: true` to `POST /boxes/{boxId}/desktop?vnc=1`, the noVNC URL does not include `_token`. The exact host, IDs, password, and token fields are generated for the running box. The Moonlight viewer removes its fragment immediately, sends the token in authenticated request headers and the first WebSocket frame, and never puts it in an HTTP or WebSocket request URL. Treat desktop URLs as sensitive because they can let the browser attach to that desktop session.

## Security model

Desktop URLs are generated through the authenticated Box API and expire after ten minutes. Box list and info responses do not cache or return a desktop URL; call the desktop endpoint each time you open a stream. The returned URL contains a desktop access token in its fragment, so do not paste the full URL into shared chats or logs. If you need to give someone else access to a box, use the intended Box account and access controls rather than sharing a desktop URL.

Desktop clipboard reads and writes are enabled only for the credentials of the active desktop Moonlight stream. Browser-only streaming does not register clipboard routes.

Stopping or archiving a box makes the desktop unavailable until the box is resumed.

Do not rely on desktop processes surviving resume or fork. After a resume or fork, reopen Chrome and restart your app or dev server. The `computer` tools come back on their own: the desktop daemon behind them is restarted with the box.

## Related

* [Computer use for the Box's agents](/box/integrated-agents#computer-use)
* [SSH Access](/box/ssh-access)
* [Long-Running Tasks](/box/long-running-tasks)
* [Hosting](/box/hosting)
* [Machine Capabilities](/box/machines)
