On Runtime, the terminal is a real one, with colors, prompts and resizing. The sandbox opens no port to provide it. The terminal travels over one authenticated WebSocket through Runtime's API. So only a key that may run commands in that sandbox can connect.
From your own terminal
Terminalruntime sandboxes shell "${id}" # an interactive terminal, like sshruntime sandboxes ssh "${id}" # the same through OpenSSH, as the sandbox userruntime sandboxes ssh "${id}" -- make test # one command; exits with its code${id} can be the sandbox's id or its name. On first use, ssh makes an SSH
key for this machine. It sends only the public half. Run
runtime sandboxes ssh config --install once. After that, every sandbox
answers as the host <id>.runtime for ssh, scp, VS Code and JetBrains Gateway
(SSH and editors).
From code
What you write is typed into the terminal. What it prints comes back as bytes, escape codes included:
TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create();const term = await sbx.terminal({ cols: 120, rows: 40, onData: (bytes) => process.stdout.write(bytes),});term.write("echo hello from the terminal\n");term.resize(100, 30);term.write("exit\n");console.log("exit code", await term.exited);Pythonfrom withruntime import Sandboxwith Sandbox.create() as sbx: term = sbx.terminal(cols=120, rows=40) term.write("echo hello from the terminal\n") term.write("exit\n") while (chunk := term.recv()) is not None: print(chunk.decode(errors="replace"), end="")In TypeScript, term.exited resolves with the shell's exit code. In Python,
recv() returns None once the terminal has closed.
The protocol, if you speak it yourself
GET /v1/sandboxes/{id}/terminal with Upgrade: websocket and the bearer
header opens one (terminals).
| Part | What it is |
|---|---|
| Query | cols, rows, command (default bash -l), cwd, or processId |
| Binary frames | Terminal bytes, both ways |
| Text frames you send | {"type":"resize","cols":120,"rows":40} |
| Text frames it sends | {"type":"ready","processId":...}, then {"type":"exit","exitCode":...} |
| Attach to a process | Start it with spawn(..., { pty: { cols, rows } }), then pass its processId |
| Limits | 64 terminals open per organization and 2 per sandbox, up to 340; 12 in one sandbox at most |
| Who you are | The runtime user, whose home is /workspace, with sudo |
| Cost | Nothing for the terminal. The sandbox's measured CPU and reserved memory |
Terminal, exec or SSH?
| You want | Use |
|---|---|
| One command and its result | exec, which waits and returns the exit code and output |
| A program that asks questions or redraws | terminal() or runtime sandboxes shell |
| Your editor, scp or rsync | runtime sandboxes ssh and ssh <id>.runtime |
| A port on your laptop | runtime sandboxes port-forward <id> <port> |
Mistakes to avoid
- Sending a command without a newline. The terminal runs what you type only
when it sees
\n, the same as a keyboard's Enter. - Parsing terminal output as plain text. It carries color and cursor codes.
For output you plan to parse, run the command with
exec. It returns cleanstdoutandstderr. - Leaving terminals open. An organization can have 64 terminals open at the
same time, and 12 in any one sandbox. Write
exit, or close the socket, when a session ends. Past the limit, you get 429 withRetry-After. - Setting a time limit for a long session. An open terminal keeps a
sandbox running. A sandbox has no time limit by default. One created with
timeoutSecondsstill ends at that limit. Leave it out, or see extend a sandbox's time limit.
Related
- Run a background process and attach a terminal to it.
- Per-user dev environments, where each user gets a shell of their own.
- Coding agent sandbox for agents that need a shell.
- An interactive terminal in the SDK reference.
Facts on this page were checked on 26 October 2026.