# Command history

> Search a machine's past commands, check how each one ended, and page through its stored stdout and stderr.

Every command run on a machine is kept in its history: [foreground commands](/docs/guides/commands), [setup scripts](/docs/guides/setup-scripts), and the starts of [background commands](/docs/guides/background-commands). Lifecycle and file operations are not. In the console, open a machine and choose **History**.

History shows command text and output, which can include values an [environment](/docs/guides/environments) exported, so it needs the same role as running commands: Owner, Admin, or Operator, or an API key with at least the `use_existing` preset. Viewers and Billing members get `403 forbidden`, and a `read_only` key gets `403 insufficient_scope`.

## List and search

```sh
curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/commands/history?q=npm&mode=sync&limit=25" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

Entries come newest first.

| Parameter | Meaning |
| --- | --- |
| `q` | Text the command contains, ignoring case. At most 128 characters, trimmed; blank means no search. |
| `mode` | `all` (default), `sync` for foreground commands and setup scripts (the console's **Commands**), or `detached` for background command starts (**Processes**). |
| `limit` | 1 to 100 entries (default 50). |
| `cursor` | The `next_cursor` of the previous page. It is opaque and continues strictly after the last entry, so commands started meanwhile never repeat or shift what you haven't read. The last page has no `next_cursor`. |

Invalid values answer `422` with the field in `field_errors`. `GET /v1/machines/{id}/commands/history/{operation_id}` returns one entry.

## What an entry says

| Field | Meaning |
| --- | --- |
| `operation_id` | The exec operation. For a background command it is also the `command_id`. |
| `mode` | `sync` or `detached`. |
| `command`, `argv` | The `/bin/sh -c` string, or the arguments joined by spaces, and the exact arguments. Search matches `command`. |
| `status`, `error_code` | The operation's status and error code, for example `failed` with `exec_nonzero` or `exec_timed_out`. See [Operations](/docs/concepts/operations). |
| `exit_code` | The exit status of a finished foreground command. |
| `timed_out`, `cancelled` | How the command ended when it didn't exit on its own. |
| `truncated` | The machine capped the captured output. |
| `stdout_bytes`, `stderr_bytes` | The size of each stored stream. |
| `output_purged` | Retention removed the stored output. The sizes stay. |
| `created_at`, `started_at`, `finished_at`, `duration_ms` | When it was requested, first started, and finished, and how long it ran across retries. Entries recorded before history existed have no `started_at`. |
| `logs_url` | Where to read the output. |

A `detached` entry describes only the start. The process's exit status and live log come from `GET /v1/machines/{id}/commands/{command_id}`, and its stored stdout and stderr are empty. In the console, **Open live status** follows it on the Terminal tab.

## Read the output

```sh
curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/commands/history/$OPERATION_ID/logs?stream=stderr&offset=0&limit=65536" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

`stream` is `stdout` (default) or `stderr`. The answer holds bytes `[offset, offset + limit)` of that stream: `{offset, next_offset, size, data_base64, complete, truncated}`. `limit` is 1 to 1,048,576 (default 65,536). Pass `next_offset` as the next `offset`; an offset at or past `size` returns no data. Output is bytes, so decode it as a stream to keep a character that two pages split whole.

While a streamed foreground command is still running, the answer serves the output relayed so far, `size` grows, and `complete` is `false`. Keep reading from `next_offset` until `complete` is `true`, then re-read the entry for its exit code.

## Retention

Stored output is removed a set number of days after a command finishes (30 by default; the server's operator chooses). The entry, its exit code, flags, timing, and sizes stay, `output_purged` becomes `true`, and reading the output answers `410 output_expired`.
