PTY sessions run over the machine WebSocket channel and also expose REST helpers for listing, resizing, and killing sessions. Idle PTY streams remain connected while the machine is running. A WebSocket close without a pty_exited message is reported as a transport disconnect. Session IDs are scoped to the current running machine execution; after hibernate/resume, create a new PTY session. PTY sessions are created over the machine WebSocket channel (/v1/machines/{id}/ws); the SDKs and CLI manage that connection. List, resize, and kill also have REST endpoints (see Manage sessions).

Create a session

session = machine.pty.create_session(cols=100, rows=30, cwd="/workspace")
chunks = []

session.send_input("pwd\nexit\n")
session.wait(on_data=lambda chunk: chunks.append(chunk.decode("utf-8")), timeout=5)
print("".join(chunks))
const session = await machine.pty.open({ cols: 100, rows: 30, cwd: "/workspace" });
session.send("pwd\nexit\n");
for await (const chunk of session.output()) {
  process.stdout.write(chunk);
}
nullspace machine pty create mch_123 --cols 120 --rows 30
# PTY sessions are created over the machine WebSocket channel.
# Connect to /v1/machines/mch_123/ws and send a pty start message.
# See ../reference/websocket-protocol for message shapes.

Disconnect and reconnect

Reconnecting to an existing session by session_id is available from the Python SDK and CLI, and over the machine WebSocket for raw clients. The TypeScript SDK opens a new session per pty.open() call and does not reconnect to an existing session.
session_id = session.session_id
session.disconnect()

reattached = machine.pty.connect(
    session_id=session_id,
    on_data=lambda chunk: print(chunk.decode("utf-8"), end=""),
)
reattached.send_input("echo reattached\n")
nullspace machine pty connect mch_123 --session-id ses_123
# Reconnect over the machine WebSocket channel using the session_id.
# Connect to /v1/machines/mch_123/ws and send a pty connect message.
# See ../reference/websocket-protocol for message shapes.
Use the session_id printed by machine pty create with --session-id. Numeric pid reconnect remains available for legacy scripts. In a local TTY, connect forwards keyboard input, streams PTY output, and tracks terminal resize events. With --json, it waits for completion, forwards piped stdin when present, and prints the final result. PTY session IDs and legacy PIDs are not durable across hibernate/resume.

Manage sessions

Listing, resizing, and killing sessions are available across all clients and use the session_id returned by create.
sessions = machine.pty.list_sessions()
session_id = sessions[0].session_id
machine.pty.resize_session(session_id, 140, 40)
machine.pty.kill_session(session_id)
const sessions = await machine.pty.listSessions();
const sessionId = sessions[0].sessionId;
await machine.pty.resizeSession(sessionId, 140, 40);
await machine.pty.killSession(sessionId);
nullspace machine pty list mch_123
nullspace machine pty resize mch_123 ses_123 --cols 140 --rows 40
nullspace machine pty kill mch_123 ses_123
curl "${NULLSPACE_API_URL}/v1/machines/mch_123/pty" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}"

curl -X POST "${NULLSPACE_API_URL}/v1/machines/mch_123/pty/ses_123/resize" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{ "cols": 140, "rows": 40 }'

curl -X DELETE "${NULLSPACE_API_URL}/v1/machines/mch_123/pty/ses_123" \
  -H "Authorization: Bearer ${NULLSPACE_API_KEY}"
The session handle exposes id, session_id, pid, is_connected, exit_code, and error. It also supports byte iteration when you want to read PTY output directly:
for chunk in session:
    print(chunk.decode("utf-8"), end="")
Use session.resize(cols, rows) for the active WebSocket handle. Use resize_session(session_id, cols, rows) when you are managing a session by ID.