Terminals (PTY)
A terminal is a real PTY shell, backed by tmux when it is present or a native PTY otherwise. The session keeps its working directory, environment, and running program alive between calls. It accepts input while that program runs.
Use it for REPLs, interactive programs, and a WebTerminal UI.
A command is different: a fresh process per call, with no shell left running afterward. Use a terminal to stay attached and watch a session live. Use a command for a plain request/response with separate stdout and stderr. The v1 and v2 routes side by side are in Migration from 1.x.
Requirements
GET /v1/capabilities shows which shell the terminal will use. exec.pty is true when that shell is bash or sh; on a PowerShell host, terminals still open but exec is unavailable.
Open a terminal
Request body:
The body takes these fields:
Request body:
The body takes these fields:
Creating up front is optional: POST /v1/shell/exec without an id opens a session and returns its id.
When a terminal needs a set size, or a resize later without an attached WebSocket, switch to v2:
Later calls use session_id to address the terminal, and working_dir is the directory the shell resolved. exec runs wherever the shell currently is — including a directory reached by a cd typed into the terminal — and an exec_dir on the request moves the shell there.
Creating under an existing id returns that session instead of creating another. A different user returns 400, as does a directory that does not exist.
Run a command
Request body:
The body takes these fields:
Request body:
The body takes these fields:
The response is the same either way:
A PTY has one output stream, so output is combined. Use the command API when stdout and stderr must remain separate.
console is the session transcript, with one entry per finished command, and holds the last 100. A terminal runs one command at a time; a second call while one is running answers 400 Session already has a running command.
status is the command's lifecycle, and exit_code is set only when it is completed:
Both timeouts interrupt with ^C and leave exit_code at null. The session stays open and takes the next command.
no_change_timeout does not apply to an async command because no call is waiting on it. A response over 30,000 bytes keeps its first and last 15,000 bytes, with [... Observation truncated due to length ...] between. The v1 route can disable this with truncate: false; the v2 route always truncates.
Routes
- The screen is the command's output so far: live while it runs, and the last output after it ends.
commandnames the running command and isnullbetween commands. waitreturns when the command ends orsecondselapses, whichever comes first. With nothing running, it answers immediately.inputreaches the PTY verbatim, including escape sequences and control characters:\u001bis ESC and\u0003is Ctrl-C.press_enterappends the carriage return sent by Enter. A raw-mode TUI treats a bare\nas Shift+Enter; do not combine a trailing\rwithpress_enter: true.- Stopping a command also ends the session. The id disappears from the listing, and a second stop answers
404. To interrupt a command but keep the terminal, usehard_timeoutorno_change_timeout. - A terminal shares the same filesystem as File. A file written at the prompt is immediately readable through the file routes, and the other way around.
Common uses
A terminal is worth its session when the work outlives one call, or when the program asks something back:
Answer a prompt
A program that asks a question runs async, and the answer is typed into it:
Aio is the envelope-aware helper from Examples: it unwraps data and raises when success is false.
A screen read right after an async exec can still catch the command line being echoed; read it again. press_enter is the one field whose default differs between the two routes, so pass it explicitly.
The WebSocket protocol
For long-running work, attach; do not poll. REST and the socket share the same shell: a file created through an exec call is immediately visible in the attached terminal. Attach to GET /v2/pty/sessions/{id}/ws, or GET /v1/shell/ws?session_id=…, on the same host and port with the ws:// scheme.
Include \n in input to run a line. cols and rows may also sit under data. Anything else — a JSON object of another shape, or plain text — is typed into the shell as raw input, so an unrecognised control frame becomes a line of garbage at the prompt.
protocol=binary carries raw PTY bytes in binary frames instead of JSON output frames; the ready frame stays JSON and reports transport.
GET /v2/pty/ws skips session creation. Its shell lives as long as the socket, announces no id, and is killed with it.
GET /v1/shell/ws without a session_id also opens a terminal, but announces its id in a session_id frame before ready. The session stays until it is closed or idles out.
Reconnect after a drop
A dropped socket does not stop the command. durable=true keeps the terminal's output while nothing is attached, and restore=true replays it on the next attach:
The build that ran on while the client was away comes back in three frames, then live output resumes:
resumed—truewhen the socket took over a terminal that was already attachedrestore_output— everything buffered while nothing was attached; a later reconnect replays only the gap since the last oneterminal_restored— the end of the replay; live output follows it
restore works only with durable. replay_bytes bounds the replay snapshot; it defaults to 10 MiB and is clamped to 256 KiB..10 MiB.
Retained output lives in daemon memory and disappears when the session is closed, killed, or reclaimed.
Without durable=true, a second socket on an attached terminal is refused with this error:
With it, the newer socket takes over and the older one closes after a relay_replaced frame.
Both flags need an explicit session; an anonymous connection carrying either answers 400.
Human in the loop
A person can open the terminal an agent is using. The prebuilt images serve a WebShell page at /terminal?session_id=…; it attaches over /v1/shell/ws, so both sides see one screen and can type.
GET /v1/shell/terminal-url builds the page link. With ?session_id= it returns the URL for an existing terminal; an unknown id is a 404. Without it, the route creates a terminal and returns its link, so repeated calls can leave unused terminals behind.
The daemon serves only the URL and socket; the page comes from the image. Browser and desktop viewers are described together on Computer Use.
Windows
A terminal on Windows uses PowerShell's ConPTY, and everything it starts runs inside a job object.
Running a command through this plane answers 501 because its completion protocol needs a POSIX shell. Create, input, screen, resize, attach, and signal still work. See Windows.
Backend and limits
AIO_SHELL_BACKEND=auto (the default) picks tmux when a working tmux binary is found.
Only an explicitly created session can use tmux. A session opened automatically by exec, a retention: "expiring" session, and an anonymous WebSocket terminal always use a native PTY. The ready frame reports the backend.
AIO_SHELL_BACKEND=native pins native PTY. tmux requires tmux and answers 503 when its binary is missing.
Session state lives in daemon memory, so ids do not survive an aiod restart. A running tmux server does survive, and its socket is reused rather than rebuilt.
Up to 20 terminals run at once, each idle-closed after 3600 s (AIO_SHELL_MAX_SESSIONS / AIO_SHELL_SESSION_TIMEOUT_SECS). A terminal with an attached WebSocket is not idle.
At the limit, the least recently used reclaimable terminal is closed to make room. When none can be reclaimed, creation answers 400.
Error handling
HTTP success only means the request was accepted. Read data.status for the command's lifecycle, then exit_code once it is completed. The failures that belong to this plane:
Closing an id that is already gone is not one of them: DELETE answers 200 with success: false. See Error Handling.