The PTY (pseudo-terminal) module allows you to create interactive terminal sessions in the sandbox with real-time, bidirectional communication.Unlike commands.run() which executes a command and returns output after completion, PTY provides:
Real-time streaming - Output is streamed as it happens via callbacks
Bidirectional input - Send input while the terminal is running
Interactive shell - Full terminal support with ANSI colors and escape sequences
Session persistence - Disconnect and reconnect to running sessions
Use sandbox.pty.create() to start an interactive bash shell. This example runs echo 'hello world', then exits and prints the terminal output.
import { Sandbox } from 'e2b'const sandbox = await Sandbox.create()const terminal = await sandbox.pty.create({ cols: 80, // Terminal size in characters rows: 24, envs: { MY_VAR: 'hello' }, // Optional environment variables cwd: '/home/user', // Optional working directory user: 'root', // Optional user to run as onData: (data) => { // Called whenever the terminal outputs data process.stdout.write(data) },})// terminal.pid contains the process IDconsole.log('Terminal PID:', terminal.pid)// Send input to the PTY. Data must be bytes (Uint8Array), and the trailing newline "presses Enter".await sandbox.pty.sendInput(terminal.pid, new TextEncoder().encode("echo 'hello world'\n"))// The PTY runs an interactive login shell that won't exit on its own, so tell it// to exit — otherwise wait() below blocks forever.await sandbox.pty.sendInput(terminal.pid, new TextEncoder().encode('exit\n'))// Wait for the terminal to exit (output is streamed to onData above)await terminal.wait()
from e2b import Sandbox, PtySizesandbox = Sandbox.create()terminal = sandbox.pty.create( size=PtySize(rows=24, cols=80), # Terminal size in characters envs={'MY_VAR': 'hello'}, # Optional environment variables cwd='/home/user', # Optional working directory user='root', # Optional user to run as)# terminal.pid contains the process IDprint('Terminal PID:', terminal.pid)# Send input to the PTY via the pty module. Data must be bytes, and the trailing newline "presses Enter".sandbox.pty.send_stdin(terminal.pid, b"echo 'hello world'\n")# The PTY runs an interactive login shell that won't exit on its own, so tell it# to exit — otherwise wait() below blocks forever.sandbox.pty.send_stdin(terminal.pid, b"exit\n")# Stream output by passing on_pty to wait()# end='' prevents print from adding an extra newlineterminal.wait(on_pty=lambda data: print(data.decode(), end=''))
The PTY runs an interactive bash shell with TERM=xterm-256color, which supports ANSI colors and escape sequences.
PTY sessions have a configurable timeout that controls the session duration. The default is 60 seconds. For interactive or long-running sessions, set timeoutMs: 0 (JavaScript) or timeout=0 (Python) to keep the session open indefinitely.
import { Sandbox } from 'e2b'const sandbox = await Sandbox.create()const terminal = await sandbox.pty.create({ cols: 80, rows: 24, onData: (data) => process.stdout.write(data), timeoutMs: 0, // Keep the session open indefinitely})
from e2b import Sandbox, PtySizesandbox = Sandbox.create()terminal = sandbox.pty.create( PtySize(rows=24, cols=80), timeout=0, # Keep the session open indefinitely)# end='' prevents print() from adding an extra newline# (PTY output already contains newlines)terminal.wait(on_pty=lambda data: print(data.decode(), end=''))
Use sendInput() in JavaScript or send_stdin() in Python to send data to the terminal. These methods return a Promise (JavaScript) or complete synchronously (Python) - the actual output is delivered to your onData callback (JavaScript) or to the on_pty callback you pass to wait() (Python).
import { Sandbox } from 'e2b'const sandbox = await Sandbox.create()const terminal = await sandbox.pty.create({ cols: 80, rows: 24, onData: (data) => process.stdout.write(data),})// Send a command (don't forget the newline!)await sandbox.pty.sendInput( terminal.pid, new TextEncoder().encode('echo "Hello from PTY"\n'))
from e2b import Sandbox, PtySizesandbox = Sandbox.create()terminal = sandbox.pty.create(PtySize(rows=24, cols=80))# Send a command as bytes (b'...' is Python's byte string syntax)# Don't forget the newline!sandbox.pty.send_stdin(terminal.pid, b'echo "Hello from PTY"\n')# Receive output by passing on_pty to wait()# end='' prevents print from adding an extra newlineterminal.wait(on_pty=lambda data: print(data.decode(), end=''))
When the user’s terminal window changes size, notify the PTY with resize(). The cols and rows parameters are measured in characters, not pixels.
import { Sandbox } from 'e2b'const sandbox = await Sandbox.create()const terminal = await sandbox.pty.create({ cols: 80, rows: 24, onData: (data) => process.stdout.write(data),})// Resize to new dimensions (in characters)await sandbox.pty.resize(terminal.pid, { cols: 120, rows: 40,})
from e2b import Sandbox, PtySizesandbox = Sandbox.create()terminal = sandbox.pty.create(PtySize(rows=24, cols=80))# Resize to new dimensions (in characters)sandbox.pty.resize(terminal.pid, PtySize(rows=40, cols=120))
You can disconnect from a PTY session while keeping it running, then reconnect later with a new data handler. This is useful for:
Resuming terminal sessions after network interruptions
Sharing terminal access between multiple clients
Implementing terminal session persistence
import { Sandbox } from 'e2b'const sandbox = await Sandbox.create()// Create a PTY sessionconst terminal = await sandbox.pty.create({ cols: 80, rows: 24, onData: (data) => console.log('Handler 1:', new TextDecoder().decode(data)),})const pid = terminal.pid// Send a commandawait sandbox.pty.sendInput(pid, new TextEncoder().encode('echo hello\n'))// Disconnect - PTY keeps running in the backgroundawait terminal.disconnect()// Later: reconnect with a new data handlerconst reconnected = await sandbox.pty.connect(pid, { onData: (data) => console.log('Handler 2:', new TextDecoder().decode(data)),})// Continue using the sessionawait sandbox.pty.sendInput(pid, new TextEncoder().encode('echo world\n'))// Wait for the terminal to exitawait reconnected.wait()
from e2b import Sandbox, PtySizesandbox = Sandbox.create()# Create a PTY sessionterminal = sandbox.pty.create(PtySize(rows=24, cols=80))pid = terminal.pid# Send a commandsandbox.pty.send_stdin(pid, b'echo hello\n')# Disconnect - PTY keeps running in the backgroundterminal.disconnect()# Later: reconnect to the same sessionreconnected = sandbox.pty.connect(pid)# Continue using the session, then exitsandbox.pty.send_stdin(pid, b'echo world\nexit\n')# Wait for the terminal to exit, streaming output via on_ptyreconnected.wait(on_pty=lambda data: print('Handler:', data.decode()))
import { Sandbox } from 'e2b'const sandbox = await Sandbox.create()const terminal = await sandbox.pty.create({ cols: 80, rows: 24, onData: (data) => process.stdout.write(data),})// Kill the PTYconst killed = await sandbox.pty.kill(terminal.pid)console.log('Killed:', killed) // true if successful// Or use the handle method// await terminal.kill()
from e2b import Sandbox, PtySizesandbox = Sandbox.create()terminal = sandbox.pty.create(PtySize(rows=24, cols=80))# Kill the PTYkilled = sandbox.pty.kill(terminal.pid)print('Killed:', killed) # True if successful# Or use the handle method# terminal.kill()
Building a fully interactive terminal (like SSH) requires handling raw mode, stdin forwarding, and terminal resize events. For a production implementation, see the E2B CLI source code which uses the same sandbox.pty API documented above.