# Files

> Browse a machine's /work as a live tree, and write, read, upload, and download files up to 32 MiB.

Files move in and out of a machine through `/work`. Paths are relative to `/work` (`notes/todo.txt`) or written in full (`/work/notes/todo.txt`). Paths can't contain `.` or `..` segments, empty segments, or NUL, and can't point anywhere else.

## Browse files in the console

A machine's **Files** tab lists `/work` live while the machine runs:

- The header shows how much data `/work` holds and how many files (**Your data 173 MiB · 301 files**). **Refresh** lists it again.
- Each row shows its size (a folder counts everything inside it) and its share of the total, with a small bar. Hover a row for its full path and modified time; the selected row shows the time inline.
- Folders open with a click or the arrow keys. The first listing goes two levels deep; deeper folders are listed when you open them.
- **Fuzzy search files…** matches names and paths in the folders listed so far (`claudeset` finds `.claude/settings.json`), opens the folders on the way to each match, and highlights the matched letters.
- **Name / Size** sorts the tree. Press the active one again to reverse it: names start A to Z with folders first, sizes start largest first.
- Keyboard: ↑ ↓ move, → opens a folder or steps into it, ← closes it or steps out, Home and End jump, Enter opens a folder or a file.

The bar under the tree acts on the selected row (or on `/work` when nothing is selected):

| Action | What it does |
| --- | --- |
| **Open** | Shows a text file under **Read a file**. |
| **Download** | Saves a file (up to 32 MiB) to this device. |
| **Download folder** | Saves a folder as a `.tar.gz` archive. Very large folders answer `413 archive_too_large`; download smaller folders or copy over [SSH](/docs/guides/ssh). |
| **Upload here** | Picks a local file and uploads it into the folder, then confirms it by reading it back. |
| **New file** | Fills **Write a file** with the folder's path. |
| **Copy path** | Copies the absolute path. |

Above the tree, **Latest snapshot** shows when the newest ready [snapshot](/docs/guides/snapshots) was taken and how much data it holds; restore or fork from it on the **Snapshots** tab. Docker development machines never push snapshots, so they show **No snapshots yet**.

A stopped machine shows **Start** in place of the tree. If a listing stops early (very large folders), the tree says so and marks the folder **Partial list**.

The listing comes from `GET /v1/machines/{id}/files/tree?path=/work&depth=2` (`path` may be `/work`, `/root`, or `/home/<user>`, or a folder inside one; `depth` is 1 to 4), which answers `{path, total_bytes, file_count, truncated, entries}`; each entry has `name`, `path`, `type` (`file`, `dir`, or `symlink`), `size_bytes`, `modified_at`, and, within the depth, `children`. Folders download from `GET /v1/machines/{id}/files/archive?path=`, and the snapshot strip reads `GET /v1/machines/{id}/snapshots/latest` (`{id, created_at, completed_at, source, tier, size_bytes}`, or `404 no_snapshot`).

## Write a file

```sh tab="CLI"
vy cp ./build.tar.gz dev:build.tar.gz
echo 'debug=true' | vy cp - dev:app/config.env
```

```ts tab="TypeScript"
await machine.files.write("app/config.json", JSON.stringify({ debug: true }));
await machine.files.write("bin/tool", new Uint8Array(bytes)); // bytes go as base64
```

```python tab="Python"
machine.files.write("app/config.json", '{"debug": true}')
machine.files.write("bin/tool", open("tool", "rb").read())  # bytes go as base64
```

```sh tab="curl"
curl -X PUT "$VMYARD_API_URL/v1/machines/$MACHINE_ID/files?path=app/config.json" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"encoding": "text", "content": "{\"debug\": true}"}'
```

`PUT /v1/machines/{id}/files?path=` takes `{encoding, content}` where `encoding` is `text` or `base64`. It replaces the whole file. Writing is an [operation](/docs/concepts/operations): the answer is `202`, and the file is written once the operation succeeds. VMYard moves files in 512 KiB chunks and checks the size the machine reports against the bytes it sent, so a partial write fails rather than passing silently.

## Read a file

```sh tab="CLI"
vy cp dev:out/report.txt .
vy cp dev:out/report.txt -      # to stdout
```

```ts tab="TypeScript"
const text = await machine.files.read("out/report.txt");
const bytes = await machine.files.readBytes("out/image.png");
```

```python tab="Python"
text = machine.files.read("out/report.txt")
data = machine.files.read_bytes("out/image.png")
```

```sh tab="curl"
curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/files?path=out/report.txt&encoding=text" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

`GET /v1/machines/{id}/files?path=&encoding=` returns `{path, size, encoding, content}`. Reads wait up to 30 seconds; if the machine hasn't answered by then, the response is `202` with an operation to follow, and the finished operation carries the content in `file_ranges`. The SDKs and CLI handle that for you.

Read with `encoding=base64` for binary files. Reading a file that isn't valid UTF-8 as `text` fails with `422 file_not_text`.

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 404 | `file_not_found` | No file at that path. |
| 413 | `file_too_large` | The file is larger than 32 MiB. |
| 422 | `file_not_text` | Not UTF-8; read it as `base64`. |
| 422 | `invalid_request` | The path is not allowed. |
| 409 | `machine_not_running` | Listing needs a running machine. |
| 413 | `archive_too_large` | The folder is too large for one archive. |

## Access

Reading and writing files needs a running machine and Owner, Admin, or Operator access, or an API key with at least the `use_existing` preset. File content is whatever the machine holds: treat it as untrusted input.
