Sandboxes

Exec

Run commands in a sandbox over HTTP or a WebSocket, with sessions that keep running when you disconnect.

Every command runs as a session. A session belongs to the sandbox, not to your connection: if the client goes away, the process keeps running and you can attach to it again.

Commands run as root with the environment and secrets of the sandbox. Exec wakes a sandbox that is not running.

Run a command over HTTP

http
POST /v1/sandboxes/{name}/exec
Query About
cmd One argument of the command. Repeat it for each one: cmd=ls&cmd=-la. Defaults to bash.
path The program to run, in place of the first cmd.
env KEY=VALUE, repeatable. Set over the environment of the sandbox.
dir The working directory.
stdin true to send the request body as stdin, up to 10 MiB.
shell
curl -X POST "$API/agent-1/exec" -H "$AUTH" --output - \
  --url-query cmd=sh --url-query cmd=-c --url-query "cmd=echo hi; ls /nope" --url-query dir=/root

--url-query (curl 7.87 and later) encodes each value for you.

The response is application/octet-stream and streams while the command runs. Each chunk starts with one byte that names its stream:

Byte Stream
1 stdout
2 stderr
3 exit: one more byte, the exit code, and the end

Pipe a file in as stdin:

shell
curl -X POST "$API/agent-1/exec?cmd=wc&cmd=-l&stdin=true" -H "$AUTH" --data-binary @notes.txt --output -

The HTTP form has no terminal. For an interactive shell, use the WebSocket.

Run a command over a WebSocket

http
GET /v1/sandboxes/{name}/exec    (Upgrade: websocket)

Takes the same query as the HTTP form, plus:

Query About
tty true for a terminal.
rows, cols The terminal size.
max_run_after_disconnect How long the process may run once no client is attached: seconds, or a number with ms, s, m or h. Without it, the process runs until it ends.

The first message is text:

json
{
  "type": "session_info",
  "session_id": 7,
  "command": "bash",
  "created": "2026-10-31T09:21:04Z",
  "cols": 120,
  "rows": 32,
  "is_owner": true,
  "tty": true,
  "output_offset": 0
}

Without a terminal, binary messages carry a stream byte and data, both ways:

Byte Direction Stream
0 to the sandbox stdin
4 to the sandbox end of stdin
1 from the sandbox stdout
2 from the sandbox stderr
3 from the sandbox exit, with the exit code as one byte

With tty=true, binary messages are raw terminal bytes in both directions, and the exit comes as text: {"type": "exit", "exit_code": 0}.

Text messages you can send:

json
{ "type": "resize", "cols": 160, "rows": 48 }
{ "type": "signal", "signal": "SIGTERM" }

The socket carries SIGTERM only. To send another signal, kill the session.

Text messages you may receive, besides session_info and exit:

Type About
error { "type": "error", "message": "..." }
gap { "type": "gap", "bytes": 4096, "output_offset": 1052672 }. Output was dropped before you attached.
behind { "type": "behind", "output_offset": ... }. The client read too slowly; the socket closes with code 1013. Attach again from that offset.

A minimal terminal client in Node, with the ws package:

javascript
import WebSocket from "ws";

const ws = new WebSocket("wss://api.nanite.run/v1/sandboxes/agent-1/exec?tty=true&cols=120&rows=32", {
  headers: { Authorization: `Bearer ${process.env.NANITE_KEY}` },
});
ws.on("message", (data, binary) => {
  if (binary) process.stdout.write(data);
  else console.log(JSON.parse(data.toString()));
});
ws.on("open", () => ws.send(Buffer.from("uname -a\n")));

Browsers cannot set headers on a WebSocket, so open sockets from your server.

List sessions

http
GET /v1/sandboxes/{name}/exec
json
{
  "sessions": [
    {
      "id": 7,
      "command": "npm run build",
      "workdir": "/root/app",
      "tty": false,
      "created": "2026-10-31T09:21:04Z",
      "is_active": true,
      "last_activity": "2026-10-31T09:22:40Z",
      "bytes_per_second": 812
    }
  ],
  "count": 1
}

A sandbox that is not running has no sessions.

Attach to a session

http
GET /v1/sandboxes/{name}/exec/{session_id}    (Upgrade: websocket)

Attaches to a session of a running sandbox and speaks the same messages as a new one. Each session keeps the last 1 MiB of its output; pass output_offset, a byte offset from session_info or from your own count, to replay from where you left off. If part of what you asked for is gone, a gap message says how much. An unknown session answers 404, and so does any session of a sandbox that is not running.

Kill a session

http
POST /v1/sandboxes/{name}/exec/{session_id}/kill
Query About
signal The signal to send first. Defaults to SIGTERM.
timeout Seconds to wait before SIGKILL. Defaults to 10.

The answer is NDJSON, one event per line, ending with complete or error:

json
{"type":"signal","message":"Signaling SIGTERM to the processes of session 7","signal":"SIGTERM","pid":412}
{"type":"exited","message":"Process exited"}
{"type":"complete","exit_code":143}

Other events are timeout (the process outlived the timeout) and killed (it was sent SIGKILL).