---
title: Terminal
description: One-shot commands as argv, interactive ptys, and why a non-zero exit code is a success.
---

Nothing here is gated on the [input lease](/docs/sdk/input#the-input-lease). A shell is a session
of its own, and two people holding separate terminals is normal.

## exec

<CodeGroup>

```ts TypeScript
const result = await machine.terminal.exec(["uname", "-sr"]);
result.stdout;
result.stderr;
result.exit_code;
```

```python Python
result = machine.terminal.exec(["uname", "-sr"])
result["stdout"]
result["stderr"]
result["exit_code"]
```

</CodeGroup>

`exec` takes **argv**, not a string. There is no shell and no tokenizing, so an argument
containing a space, a quote or a semicolon is one argument:

<CodeGroup>

```ts TypeScript
await machine.terminal.exec(["git", "commit", "-m", "a message; with a semicolon"]);
```

```python Python
machine.terminal.exec(["git", "commit", "-m", "a message; with a semicolon"])
```

</CodeGroup>

That is the whole defence against command injection, and it works only because the API refuses to
split a string for you.

| Prop | Type | Default | Description |
| - | - | - | - |
| `timeout?` | `number` | - | TypeScript `timeoutMs` (milliseconds) / Python `timeout` (seconds). How long the command may run. |
| `user?` | `"agent" \| "root"` | `"agent"` | The two users the image ships. `agent` owns the desktop. |
| `cwd?` | `string` | - | Working directory. |
| `stdin?` | `string` | - | Fed to the process. |

## A non-zero exit code is a successful call

Only a failure to run the command **at all** raises. A command that runs and returns 1 comes back
normally with `exit_code: 1` — the code is data, not a fault.

<CodeGroup>

```ts TypeScript
const found = await machine.terminal.exec(["grep", "-q", "needle", "/tmp/haystack"]);
if (found.exit_code === 1) {
  // not an error: grep says "no match"
}
```

```python Python
found = machine.terminal.exec(["grep", "-q", "needle", "/tmp/haystack"])
if found["exit_code"] == 1:
    # not an error: grep says "no match"
    ...
```

</CodeGroup>

Worth holding onto in an agent loop. A model that sees an exception on every failed grep learns to
avoid grep; a model that sees an exit code learns to read it.

## Asking for a shell by name

`exec` deliberately does not tokenize, so pipes, globs and redirects need a shell, spelled out:

<CodeGroup>

```ts TypeScript
await machine.terminal.execShell("cd /tmp/site && python3 -m http.server 3000 &");
// exactly equivalent to:
await machine.terminal.exec(["/bin/sh", "-lc", "cd /tmp/site && python3 -m http.server 3000 &"]);
```

```python Python
machine.terminal.exec_shell("cd /tmp/site && python3 -m http.server 3000 &")
# exactly equivalent to:
machine.terminal.exec(["/bin/sh", "-lc", "cd /tmp/site && python3 -m http.server 3000 &"])
```

</CodeGroup>

The point is that a reader can see you asked for it. Guessing where a command ends is how
injection happens.

## Interactive terminals

<CodeGroup>

```ts TypeScript
const term = await machine.terminal.open({ cols: 120, rows: 32 });
const open = await machine.terminal.list();
await machine.terminal.kill(term.id);
```

```python Python
term = machine.terminal.open(cols=120, rows=32)
open_terms = machine.terminal.list()
machine.terminal.kill(term["id"])
```

</CodeGroup>

`open` takes `command`, `cwd`, `user`, `cols` and `rows`. To attach and exchange bytes, see
[streams](/docs/sdk/streams#terminals).

Killing a pty is also what closing an attached connection does.
