# Interactive terminal (PTY) (/docs/sandbox/pty)

<!-- agent-signals: reading_time_min: 7 · est_tokens: 3662 · updated: 2026-07-30 -->
Related: [Auto-resume on request](/docs/sandbox/auto-resume.md), [Connect to running sandbox](/docs/sandbox/connect.md), [Environment variables](/docs/sandbox/environment-variables.md), [Filesystem-only snapshots](/docs/sandbox/filesystem-only-snapshots.md), [Sandbox forking](/docs/sandbox/fork.md), [Git integration](/docs/sandbox/git-integration.md)

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

## Create a PTY session [#create-a-pty-session]

Use `sandbox.pty.create()` to start an interactive bash shell. This example runs `echo 'hello world'`, then exits and prints the terminal output.

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```js  
      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 ID
      console.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()
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      from e2b import Sandbox, PtySize

      sandbox = 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 ID
      print('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 newline
      terminal.wait(on_pty=lambda data: print(data.decode(), end=''))
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

<Note>
  The PTY runs an interactive bash shell with `TERM=xterm-256color`, which supports ANSI colors and escape sequences.
</Note>

## Timeout [#timeout]

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.

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```js  
      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
      })
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      from e2b import Sandbox, PtySize

      sandbox = 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=''))
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Send input to PTY [#send-input-to-pty]

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).

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```js  
      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')
      )
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      from e2b import Sandbox, PtySize

      sandbox = 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 newline
      terminal.wait(on_pty=lambda data: print(data.decode(), end=''))
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Resize the terminal [#resize-the-terminal]

When the user's terminal window changes size, notify the PTY with `resize()`. The `cols` and `rows` parameters are measured in characters, not pixels.

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```js  
      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,
      })
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      from e2b import Sandbox, PtySize

      sandbox = 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))
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Disconnect and reconnect [#disconnect-and-reconnect]

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

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```js  
      import { Sandbox } from 'e2b'

      const sandbox = await Sandbox.create()

      // Create a PTY session
      const 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 command
      await sandbox.pty.sendInput(pid, new TextEncoder().encode('echo hello\n'))

      // Disconnect - PTY keeps running in the background
      await terminal.disconnect()

      // Later: reconnect with a new data handler
      const reconnected = await sandbox.pty.connect(pid, {
        onData: (data) => console.log('Handler 2:', new TextDecoder().decode(data)),
      })

      // Continue using the session
      await sandbox.pty.sendInput(pid, new TextEncoder().encode('echo world\n'))

      // Wait for the terminal to exit
      await reconnected.wait()
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      from e2b import Sandbox, PtySize

      sandbox = Sandbox.create()

      # Create a PTY session
      terminal = sandbox.pty.create(PtySize(rows=24, cols=80))

      pid = terminal.pid

      # Send a command
      sandbox.pty.send_stdin(pid, b'echo hello\n')

      # Disconnect - PTY keeps running in the background
      terminal.disconnect()

      # Later: reconnect to the same session
      reconnected = sandbox.pty.connect(pid)

      # Continue using the session, then exit
      sandbox.pty.send_stdin(pid, b'echo world\nexit\n')

      # Wait for the terminal to exit, streaming output via on_pty
      reconnected.wait(on_pty=lambda data: print('Handler:', data.decode()))
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Kill the PTY [#kill-the-pty]

Terminate the PTY session with `kill()`.

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```js  
      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 PTY
      const killed = await sandbox.pty.kill(terminal.pid)
      console.log('Killed:', killed)  // true if successful

      // Or use the handle method
      // await terminal.kill()
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      from e2b import Sandbox, PtySize

      sandbox = Sandbox.create()

      terminal = sandbox.pty.create(PtySize(rows=24, cols=80))

      # Kill the PTY
      killed = sandbox.pty.kill(terminal.pid)
      print('Killed:', killed)  # True if successful

      # Or use the handle method
      # terminal.kill()
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Wait for PTY to exit [#wait-for-pty-to-exit]

Use `wait()` to wait for the terminal session to end (e.g., when the user types `exit`).

<CodeGroup>
  <CodeBlockTabs defaultValue="JavaScript & TypeScript" groupId="javascript-typescript+python">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="JavaScript & TypeScript">
        JavaScript & TypeScript
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="Python">
        Python
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="JavaScript & TypeScript">
      ```js  
      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 exit command
      await sandbox.pty.sendInput(terminal.pid, new TextEncoder().encode('exit\n'))

      // Wait for the terminal to exit
      const result = await terminal.wait()
      console.log('Exit code:', result.exitCode)
      ```
    </CodeBlockTab>

    <CodeBlockTab value="Python">
      ```python  
      from e2b import Sandbox, PtySize

      sandbox = Sandbox.create()

      terminal = sandbox.pty.create(PtySize(rows=24, cols=80))

      # Send exit command
      sandbox.pty.send_stdin(terminal.pid, b'exit\n')

      # Wait for the terminal to exit, streaming output via on_pty
      result = terminal.wait(on_pty=lambda data: print(data.decode(), end=''))
      print('Exit code:', result.exit_code)
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Interactive terminal (SSH-like) [#interactive-terminal-ssh-like]

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](https://github.com/e2b-dev/E2B/blob/main/packages/cli/src/terminal.ts) which uses the same `sandbox.pty` API documented above.
