# Pseudo Terminal (PTY)

Daytona provides pseudo terminal (PTY) capabilities through the process module in sandboxes. A PTY is a process started with the PTY kind: it runs under a virtual terminal so programs behave as if connected to a real terminal, with terminal size, line discipline, and interactive prompts.

PTY processes are exposed through a sandbox's PTY namespace and share the handle API of other processes, described in [process execution](https://www.daytona.io/docs/en/v2/process-execution.md).

## Use cases

PTY sessions are used when a program expects a real terminal.

- **Interactive development**: REPLs, debuggers, and editors that read keystrokes and redraw the screen.
- **Terminals in web applications**: a browser terminal connects to a PTY session, sends keystrokes, and resizes the session with the window.
- **Builds and tests**: compilers, test runners, and deploy tools that show progress or colors only when attached to a terminal.
- **System administration**: setup tools and package managers that ask questions before they continue.
- **Interrupts**: a long-running process receives Ctrl+C through the terminal and stops cleanly.
- **Reconnects**: a client that reconnects replays the output from a cursor and sees what it missed.
- **Shared sessions**: a named session is reachable by its name from another client. Clients attached to one session at the same time share its shell, keystrokes, and output.
- **Terminal agents**: an agent works in one interactive shell that keeps its working directory and variables between commands. It reads the output as a terminal shows it, including escape sequences.

## Start PTY session

Start a PTY session. Without a command, the session runs an interactive shell: the login shell of the user it runs as, or the first of bash, zsh, and sh found in the image. With a command, the session runs that command in the terminal instead. The terminal starts at 80 columns by 24 rows unless you set a size, and you can [resize](#resize-pty-session) it later.

The SDKs and the CLI keep an exited session's record and output until [deleted](#kill-pty-session).


```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

# Without a command the PTY runs an interactive shell; the terminal defaults to 80x24
handle = sandbox.pty.start()
print(handle.id)
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

// Without a command the PTY runs an interactive shell; the terminal defaults to 80x24
const handle = await sandbox.pty.start()
console.log(handle.id)
```


```bash
# The CLI needs a command, so this starts bash; requires a terminal, and the PTY takes its size
daytona sandbox exec {sandboxId} --tty -- bash
```

| **Flag**    | **Type** | **Description**                                                                                                                                  |
| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`--timeout`** | **`string`** | Limits how long **`exec`** stays attached. When the limit elapses, the CLI exits with code **`8`** and the session keeps running. This is a global flag. The default is **`10m`**. |

- The CLI runs the arguments after **`--`** as a program, without a shell.
- Wrap shell syntax in **`bash -c '...'`**.
- When the session ends, the CLI exits with the exit code of the process.
- The CLI cannot delete the session record and its output, so use an SDK or the API.


```bash
# Without shellCommand the PTY runs an interactive shell; terminal defaults to 80x24.
# The SDKs set keepLogs to until_cleanup; the API default for unnamed processes is on_exit_ttl.
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{"kind": "pty", "stdin": "open", "keepLogs": "until_cleanup"}'
```



```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

handle = sandbox.pty.start("python3 -i", cols=100, rows=40)
```

| **Parameter** | **Type** | **Description** |
| ------------- | -------- | --------------- |
| **`command`** | **`str`** | Sets the command to run in the PTY. |
| **`cols`** | **`int`** | Sets the number of terminal columns. The value must be at least **`1`**. The default is **`80`**. |
| **`rows`** | **`int`** | Sets the number of terminal rows. The value must be at least **`1`**. The default is **`24`**. |
| **`cwd`** | **`str`** | Sets the working directory of the command. |
| **`env`** | **`Dict[str, str]`** | Sets environment variables for the command. |
| **`name`** | **`str`** | Sets the session name. See [name PTY session](#name-pty-session). |

- The Python **`pty.start()`** always opens stdin.
- The Python **`pty.start()`** keeps the record and output until the process is deleted.


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

const handle = await sandbox.pty.start('python3 -i', {
  terminal: { cols: 100, rows: 40 },
})
```

| **Parameter** | **Type**       | **Description**                                                                                                                                  |
| ------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`terminal`**    | **`TerminalSize`** | Sets the terminal size. **`cols`** and **`rows`** must each be at least **`1`**. The default is 80 columns by 24 rows. |
| **`keepLogs`**    | **`KeepLogs`**     | Sets the log retention policy. Pass another policy to have the record removed after exit. See [log retention](https://www.daytona.io/docs/en/v2/log-streaming.md#log-retention). The default is **`'until_cleanup'`**. |


```bash
# The PTY takes the size of the local terminal; the CLI cannot set cols and rows
daytona sandbox exec {sandboxId} --tty -- python3 -i
```


```bash
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{
  "kind": "pty",
  "shellCommand": "python3 -i",
  "stdin": "open",
  "terminal": {"cols": 100, "rows": 40}
}'
```

| **Field**  | **Type** | **Description**                                              |
| ---------- | -------- | ------------------------------------------------------------ |
| **`terminal`** | **`object`** | Sets the terminal size with **`cols`** and **`rows`**. Each must be at least **`1`**, and **`0`** returns **`400`**. The field is accepted only with **`kind: "pty"`**. |


## Name PTY session

Name a PTY session. A name aliases the session ID, so another client can reach the session without knowing its ID. The name belongs to the session only while it runs: when the session exits, the name is released for reuse, and the session stays reachable by its ID only.


```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

handle = sandbox.pty.start(name="dev-shell")

# Names are unique among live processes and resolve wherever an ID is accepted
info = sandbox.process.get("dev-shell")
```

- Names use letters, digits, **`.`**, **`_`**, and **`-`**, up to 128 characters.
- The names **`entrypoint`** and **`dockerd`** are reserved for system processes.


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

const handle = await sandbox.pty.start(undefined, { name: 'dev-shell' })

// Names are unique among live processes and resolve wherever an ID is accepted
const info = await sandbox.process.get('dev-shell')
```

- Names use letters, digits, **`.`**, **`_`**, and **`-`**, up to 128 characters.
- The names **`entrypoint`** and **`dockerd`** are reserved for system processes.


```bash
# A second live process with the same name is rejected with 409 NAME_CONFLICT
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{"kind": "pty", "name": "dev-shell", "stdin": "open"}'

# The name works as an alias for the process ID on every /processes/{id} route
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/dev-shell' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

- Names use letters, digits, **`.`**, **`_`**, and **`-`**, up to 128 characters.
- The names **`entrypoint`** and **`dockerd`** are reserved for system processes.


## Configure shell, user, and timeout

Configure the shell, the user, and the timeout of a PTY session. A shell that cannot be found or a user that does not exist fails the start. When the timeout elapses, the session is killed and reports that it timed out. The Python SDK does not set these options for PTY sessions.


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

const handle = await sandbox.pty.start(undefined, {
  shell: '/bin/bash', // default: the user's login shell, then bash, zsh, sh
  user: 'root', // run as another user
  executionTimeout: 600_000, // SIGKILL after 10 minutes; wait() reports reason 'timed_out'
})
```

| **Parameter** | **Type** | **Description**                               |
| ------------- | -------- | --------------------------------------------- |
| **`shell`**       | **`string`** | Sets the shell, as a path or a name looked up on **`PATH`**. |
| **`user`**        | **`string`** | Sets the user the session runs as. The user must exist in **`/etc/passwd`** of the sandbox. |


```bash
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{
  "kind": "pty",
  "shell": "/bin/bash",
  "user": "root",
  "timeoutMs": 600000,
  "stdin": "open"
}'
```

| **Field**   | **Type**  | **Description**                                                                                        |
| ----------- | --------- | ------------------------------------------------------------------------------------------------------ |
| **`timeoutMs`** | **`integer`** | Sets the session timeout in milliseconds. When the timeout elapses, the daemon sends **`SIGKILL`** and the record reports **`reason: timed_out`**. |


## Connect to PTY session

Connect to an existing PTY session by its ID. Connecting creates a handle without contacting the sandbox; the handle sends input to the session, reads its output, resizes it, and stops it. A live connection streams output produced after it opens; to see earlier output, replay it from a cursor.


```python
handle = sandbox.pty.connect(process_id)
handle.stdin('echo "Reconnected"\n')

# The Python SDK has no WebSocket attach; handle.attach() raises
# UnsupportedOperationError. Use stdin() and stream_logs() instead.
```


```typescript
const handle = sandbox.pty.connect(processId)
const connection = await handle.attach()

const decoder = new TextDecoder()
;(async () => {
  for await (const chunk of connection.readable) {
    process.stdout.write(decoder.decode(chunk))
  }
})()

connection.write('echo "Reconnected"\n')
```

- **`attach()`** opens a WebSocket and needs a global **`WebSocket`** in the runtime.
- Read earlier output with **`logs()`**.
- If the session has already exited, the connection closes without output.


```bash
daytona sandbox process attach {sandboxId} {processId}

# Replay output from a previous log cursor before the live stream
daytona sandbox process attach {sandboxId} {processId} --cursor {cursor}
```

| **Flag**   | **Type** | **Description**                                                 |
| ---------- | -------- | --------------------------------------------------------------- |
| **`--cursor`** | **`string`** | Replays output from a previous log cursor before the live stream. Without this flag, the command shows only output produced after the attach. |

- When the session ends, the CLI exits with the exit code of the process.


```bash
# WebSocket endpoint: binary frames carry PTY output,
# text or binary client messages are written to stdin.
# Authenticate with an Authorization: Bearer header or ?rlp_token= (browsers).
wss://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}/attach

# Pass ?cursor= to replay output from a previous log cursor before the live stream.
# Close codes: 1008 "cursor expired" | "invalid cursor" | "slow consumer";
# a plain close when the process is already terminal (read scrollback from /logs).
wss://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}/attach?cursor={cursor}
```


## List PTY sessions

List PTY sessions, including retained exited ones; check each session's state.


```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

processes = sandbox.pty.list()
for info in processes:
    print(f"PTY: {info.id} ({info.state})")
    if info.terminal:
        print(f"  Size: {info.terminal.cols}x{info.terminal.rows}")
    print(f"  Working directory: {info.cwd}")
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

const processes = await sandbox.pty.list()
for (const info of processes.items) {
  console.log(`PTY: ${info.id} (${info.state})`)
  if (info.terminal) {
    console.log(`  Size: ${info.terminal.cols}x${info.terminal.rows}`)
  }
  console.log(`  Working directory: ${info.cwd}`)
}
```


```bash
daytona sandbox process list {sandboxId} --kind pty
```


```bash
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes?kind=pty' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```


## Get PTY session info


```python
info = handle.get()
print(f"PTY: {info.id}")
print(f"State: {info.state}")
print(f"PID: {info.pid}")
if info.terminal:
    print(f"Size: {info.terminal.cols}x{info.terminal.rows}")
print(f"Working directory: {info.cwd}")
if info.state == "terminal":
    print(f"Exit code: {info.exit_code}")
```


```typescript
const info = await handle.get()
console.log(`PTY: ${info.id}`)
console.log(`State: ${info.state}`)
console.log(`PID: ${info.pid}`)
console.log(`Size: ${info.terminal?.cols}x${info.terminal?.rows}`)
console.log(`Working directory: ${info.cwd}`)
if (info.state === 'terminal') {
  console.log(`Exit code: ${info.exitCode}`)
}
```


```bash
daytona sandbox process get {sandboxId} {processId}
```


```bash
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```


## Kill PTY session

Stop a PTY session and remove it. Stopping sends a signal to the session's processes; removing deletes the session record and its output. Removing a session that is still running kills it first.


```python
# Sends SIGTERM; delete() removes the process record and logs
handle.kill()
handle.delete()
```

| **Parameter**    | **Type**     | **Description**                                                                                     |
| ---------------- | ------------ | --------------------------------------------------------------------------------------------------- |
| **`signal`**         | **`SignalName`** | Sets the signal that **`kill()`** sends: **`TERM`**, **`KILL`**, **`INT`**, **`HUP`**, **`QUIT`**, **`USR1`**, or **`USR2`**. The default is **`"TERM"`**. |
| **`escalate_after`** | **`float`**      | Sends **`escalate_to`** if the session is still running after this delay. The delay is in seconds, at most **`600`**. |
| **`escalate_to`**    | **`SignalName`** | Sets the signal sent after **`escalate_after`**. The default is **`"KILL"`**. |

- On a running session, **`delete()`** sends **`SIGKILL`**.
- **`delete()`** waits up to 3 seconds for the exit, then removes the session.


```typescript
// Sends SIGTERM; delete() removes the process record and logs
await handle.kill()
await handle.delete()
```

| **Parameter**   | **Type**        | **Description**                                                                                             |
| --------------- | --------------- | ----------------------------------------------------------------------------------------------------------- |
| **`signal`**        | **`ProcessSignal`** | Sets the signal that **`kill()`** sends: **`'TERM'`**, **`'KILL'`**, **`'INT'`**, **`'HUP'`**, **`'QUIT'`**, **`'USR1'`**, or **`'USR2'`**. |
| **`escalateAfter`** | **`number`**        | Sends **`escalateTo`** if the session is still running after this delay. The delay is in milliseconds, at most **`600000`**. |
| **`escalateTo`**    | **`ProcessSignal`** | Sets the signal sent after **`escalateAfter`**. The default is **`'KILL'`**. |

- On a running session, **`delete()`** sends **`SIGKILL`**.
- **`delete()`** waits up to 3 seconds for the exit, then removes the session.


```bash
# Send a signal
daytona sandbox process signal {sandboxId} {processId} TERM

# Or send SIGKILL; the CLI cannot remove the process record and logs
daytona sandbox process kill {sandboxId} {processId}
```

| **Flag**           | **Type** | **Description**                                                                                     |
| ------------------ | -------- | --------------------------------------------------------------------------------------------------- |
| **`--escalate-after`** | **`string`** | Sends **`--escalate-to`** if the session is still running after this delay. The delay is a duration such as **`5s`**, at most **`10m`**. |
| **`--escalate-to`**    | **`string`** | Sets the second signal, sent after **`--escalate-after`**. |

- Signal names have no **`SIG`** prefix: **`TERM`**, **`KILL`**, **`INT`**, **`HUP`**, **`QUIT`**, **`USR1`**, or **`USR2`**.


```bash
# Send a signal
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}/signals' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{"signal": "SIGTERM"}'

# Remove the process record and logs
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}' \
  --request DELETE \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

| **Field**         | **Type**  | **Description**                                                                                   |
| ----------------- | --------- | ------------------------------------------------------------------------------------------------- |
| **`signal`**          | **`string`**  | Sets the signal to send, with the **`SIG`** prefix. An empty value sends **`SIGTERM`**. |
| **`escalateAfterMs`** | **`integer`** | Sends **`escalateTo`** if the session is still running after this delay. The delay is at most **`600000`**. |
| **`escalateTo`**      | **`string`**  | Sets the signal sent after **`escalateAfterMs`**. The default is **`SIGKILL`**. |

- **`DELETE`** on a running session sends **`SIGKILL`** first.


### Delete a PTY session

Delete a PTY session in one step: a running session gets a termination signal, then the session record and its output are removed. Deleting a non-PTY process fails and leaves it running.


```python
sandbox.pty.delete(handle.id)
```


```typescript
await sandbox.pty.delete(handle.id)
```


## Resize PTY session

Resize the terminal of a running PTY session. The program in the terminal receives a window size change signal and can redraw at the new size. The session record reports the new size.


```python
handle.resize(cols=150, rows=40)
```

- **`cols`** and **`rows`** must be at least **`1`**.
- You can resize only PTY sessions.


```typescript
await handle.resize(150, 40)
```

- **`cols`** and **`rows`** must be at least **`1`**.
- You can resize only PTY sessions.


```bash
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}/resize' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{"cols": 150, "rows": 40}'
```

- **`cols`** and **`rows`** must be at least **`1`**.
- You can resize only PTY sessions.


## Send input

Send input to a PTY session. Input reaches the terminal as if typed, so end each command with a newline. Control keys are bytes: send Ctrl+C as 0x03 and Ctrl+D as 0x04. A terminal has no separate end of input; to end input, send Ctrl+D.


```python
handle.stdin("ls -la\n")
```

| **Parameter** | **Type**            | **Description**                                         |
| ------------- | ------------------- | ------------------------------------------------------- |
| **`data`**        | **`Union[str, bytes]`** | Sets the input to send, as **`str`** or **`bytes`**. Bytes are sent base64 encoded. This parameter is required. |


```typescript
await handle.stdin('ls -la\n')
```

| **Parameter** | **Type**                  | **Description**                                                       |
| ------------- | ------------------------- | --------------------------------------------------------------------- |
| **`data`**        | **`string \| Uint8Array`**    | Sets the input to send, as a **`string`** or a **`Uint8Array`**. A **`Uint8Array`** is sent base64 encoded. This parameter is required. |


```bash
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}/stdin' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{"data": "ls -la\n"}'
```

| **Field**  | **Type** | **Description**                    |
| ---------- | -------- | ---------------------------------- |
| **`encoding`** | **`string`** | Sets the input encoding, **`text`** or **`base64`**. The default is **`text`**. |


## Read output

Read one page of PTY output. A page holds up to 1,000 frames by default. To read past the first page, continue from a cursor as shown in [replay output from a cursor](#replay-output-from-a-cursor).


```python
page = handle.logs()
for frame in page.items:
    print(frame.data, end="")
```

| **Parameter** | **Type**      | **Description**                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------- |
| **`limit`**       | **`int`**         | Sets the page size. The daemon clamps the page size to **`1`** to **`5000`**. The default is **`1000`**. |
| **`encoding`**    | **`LogEncoding`** | Sets the output encoding. In text encoding, bytes that are not valid UTF-8 are replaced. Pass **`"base64"`** to read raw bytes. The default is **`"text"`**. |


```typescript
const page = await handle.logs()
for (const frame of page.items) {
  process.stdout.write(frame.data)
}
```

| **Parameter** | **Type** | **Description**                                                                                     |
| ------------- | -------- | --------------------------------------------------------------------------------------------------- |
| **`limit`**       | **`number`** | Sets the page size. The daemon clamps the page size to **`1`** to **`5000`**. The default is **`1000`**. |
| **`encoding`**    | **`string`** | Sets the output encoding. In text encoding, bytes that are not valid UTF-8 are replaced. Pass **`'base64'`** to read raw bytes. |


```bash
daytona sandbox process logs {sandboxId} {processId}
```


```bash
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}/logs' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

| **Field**  | **Type**  | **Description**                                                                                                  |
| ---------- | --------- | ---------------------------------------------------------------------------------------------------------------- |
| **`limit`**    | **`integer`** | Sets the page size. The value is clamped to **`1`** to **`5000`**. The default is **`1000`**. |
| **`encoding`** | **`string`**  | Sets the output encoding, **`text`** or **`base64`**. Text replaces bytes that are not valid UTF-8. Base64 output is unpadded base64url. The default is **`text`**. |


## Replay output from a cursor

Replay PTY output from a cursor. Every output frame carries a cursor, and a read with a cursor returns the frames after it, so a client that saved the last cursor it processed can resume without gaps or duplicates. Retention can evict old output; a cursor that points into evicted output fails, and the error carries the oldest cursor still available. See [log retention](https://www.daytona.io/docs/en/v2/log-streaming.md#log-retention).


```python
# Every frame carries a cursor; reading resumes strictly after it. An empty page means the read has caught up
cursor = last_cursor
while True:
    page = handle.logs(cursor=cursor)
    if not page.items:
        break
    for frame in page.items:
        print(frame.data, end="")
        cursor = frame.cursor
```


```typescript
// Every frame carries a cursor; reading resumes strictly after it. An empty page means the read has caught up
let cursor = lastCursor
for (;;) {
  const page = await handle.logs({ cursor })
  if (page.items.length === 0) break
  for (const frame of page.items) {
    process.stdout.write(frame.data)
    cursor = frame.cursor
  }
}
```


```bash
daytona sandbox process logs {sandboxId} {processId} --cursor {cursor}
```


```bash
# An evicted cursor returns 409 CURSOR_EXPIRED with firstAvailableCursor
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}/logs?cursor={cursor}' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```


## Wait for completion

Wait for a PTY session to exit. The result holds the exit code, the ending signal if any, and a reason: exited, killed by a signal, timed out, failed to start, or lost. If the wait time elapses first, the result reports a timeout with no exit code, and the session keeps running.


```python
result = handle.wait(timeout=30)
print(result.reason, result.exit_code)
```

| **Parameter** | **Type** | **Description**                                                   |
| ------------- | -------- | ----------------------------------------------------------------- |
| **`timeout`**     | **`float`**  | Sets how long to wait, in seconds. Omit it or pass **`0`** to wait until the session exits. |


```typescript
const result = await handle.wait({ timeout: 30000 })
console.log(result.reason, result.exitCode)
```

| **Parameter** | **Type** | **Description**                                                       |
| ------------- | -------- | --------------------------------------------------------------------- |
| **`timeout`**     | **`number`** | Sets how long to wait, in milliseconds. Omit it or pass **`0`** to wait until the session exits. |


```bash
daytona sandbox process wait {sandboxId} {processId} --timeout 30s
```

- The CLI exits with the exit code of the session.


```bash
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/processes/{processId}/wait?timeoutMs=30000' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

| **Field**   | **Type**  | **Description**                                      |
| ----------- | --------- | ---------------------------------------------------- |
| **`timeoutMs`** | **`integer`** | Sets how long to wait, in milliseconds. **`0`** waits until the session exits. The default is **`60000`**. |

- When the wait time elapses first, the response is **`{"reason": "timed_out"}`** with no exit code.


## Interactive shell session

Run several commands in one interactive shell. The shell keeps its state between commands, such as the working directory and variables. Read output while you send input; the output stream ends after the shell exits and all of its output is delivered.


```python
import threading

from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

handle = sandbox.pty.start()

# Print output in the background
def pump():
    for frame in handle.stream_logs():
        print(frame.data, end="")

reader = threading.Thread(target=pump, daemon=True)
reader.start()

for command in [
    "pwd",
    "ls -la",
    "echo 'Hello from PTY'",
    "python3 -c 'print(2 + 2)'",
    "exit",
]:
    handle.stdin(command + "\n")

result = handle.wait()
reader.join(timeout=5)
print(f"\nShell exited with code: {result.exit_code}")
handle.delete()
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

const { handle, connection } = await sandbox.pty.attach()

const decoder = new TextDecoder()
const reader = (async () => {
  for await (const chunk of connection.readable) {
    process.stdout.write(decoder.decode(chunk))
  }
})()

for (const command of [
  'pwd',
  'ls -la',
  "echo 'Hello from PTY'",
  "python3 -c 'print(2 + 2)'",
  'exit',
]) {
  connection.write(command + '\n')
}

const result = await handle.wait()
await reader
if (result.reason === 'exited') {
  console.log(`\nShell exited with code: ${result.exitCode}`)
}
await handle.delete()
```


## Long-running process with interrupt

Interrupt a running command from the terminal. Ctrl+C sent as input goes through the terminal, which interrupts the foreground command; the shell itself keeps running. Signaling the session instead targets the whole session, including the shell. See [kill PTY session](#kill-pty-session).


```python
import time

from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

handle = sandbox.pty.start()
handle.stdin("for i in $(seq 1 100); do echo \"Line $i\"; sleep 1; done\n")

time.sleep(5)
handle.stdin("\x03")  # Ctrl+C
handle.stdin("exit\n")

result = handle.wait()
for frame in handle.logs().items:
    print(frame.data, end="")
print(f"\nExit code: {result.exit_code}")
handle.delete()
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

const { handle, connection } = await sandbox.pty.attach()

const decoder = new TextDecoder()
;(async () => {
  for await (const chunk of connection.readable) {
    process.stdout.write(decoder.decode(chunk))
  }
})()

connection.write('for i in $(seq 1 100); do echo "Line $i"; sleep 1; done\n')

await new Promise((resolve) => setTimeout(resolve, 5000))
connection.write('\x03') // Ctrl+C
connection.write('exit\n')

const result = await handle.wait()
if (result.reason === 'exited') {
  console.log(`\nExit code: ${result.exitCode}`)
}
await handle.delete()
```


## Resource management

Remove a PTY session when you no longer need it. The SDKs keep an exited session's record and output until it is deleted, so delete it in a final block to remove it even when a step fails.


```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

handle = sandbox.pty.start()
try:
    handle.stdin("echo hello\n")
    handle.stdin("exit\n")
    handle.wait(timeout=10)
finally:
    handle.delete()
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

const { handle, connection } = await sandbox.pty.attach()
try {
  connection.write('echo hello\n')
  connection.write('exit\n')
  await handle.wait({ timeout: 10000 })
} finally {
  connection.close()
  await handle.delete()
}
```