> ## Documentation Index
> Fetch the complete documentation index at: https://docs.herm.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Files

> List, read, write, move, copy, search, and delete files on a subject's persistent sandbox volume.

The Files API reads and writes the durable workspace for one agent and one
stable `subjectId`. That workspace is the same sandbox volume the agent mounts
during sessions and automations.

You do not need an active session. All endpoints require an `x-api-key` header.
Paths are workspace-relative, use `/` separators, and must not contain `..`.
A leading slash is optional and is stripped.

See [Files and workspaces](/guides/files) for volume layout, reserved paths, and
when to use attachments or memory instead.

## List files

```
GET /v1/agents/{agentId}/users/{subjectId}/files
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "https://api.herm.run/v1/agents/agent_123/users/user_123/files?path=reports&recursive=true" \
  -H "x-api-key: $HERM_API_KEY"
```

| Query       | Type    | Required | Description                                                             |
| ----------- | ------- | -------- | ----------------------------------------------------------------------- |
| `path`      | string  | No       | Directory to list. Defaults to the workspace root.                      |
| `recursive` | boolean | No       | When `true`, include nested files and directories. Defaults to `false`. |
| `limit`     | integer | No       | Page size, 1–100. Defaults to `50`.                                     |
| `offset`    | integer | No       | Number of entries to skip. Defaults to `0`.                             |

A subject with no volume yet returns an empty list, not `404`.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "object": "list",
  "path": "reports",
  "data": [
    {
      "object": "file",
      "path": "reports/q3.md",
      "name": "q3.md",
      "type": "file",
      "sizeBytes": 2048,
      "mimeType": "text/markdown",
      "checksumSha256": "4f3c0a1b2d8e9f6a7c5b4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a",
      "createdAt": "2026-08-13T18:00:00.000Z",
      "updatedAt": "2026-08-13T18:05:00.000Z"
    },
    {
      "object": "file",
      "path": "reports/charts",
      "name": "charts",
      "type": "directory",
      "sizeBytes": 0,
      "mimeType": null,
      "checksumSha256": null,
      "createdAt": "2026-08-13T18:01:00.000Z",
      "updatedAt": "2026-08-13T18:01:00.000Z"
    }
  ]
}
```

## Get file metadata

```
GET /v1/agents/{agentId}/users/{subjectId}/files/stat
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "https://api.herm.run/v1/agents/agent_123/users/user_123/files/stat?path=reports/q3.md" \
  -H "x-api-key: $HERM_API_KEY"
```

| Query  | Type   | Required | Description                                |
| ------ | ------ | -------- | ------------------------------------------ |
| `path` | string | Yes      | Workspace-relative file or directory path. |

The response is a single `file` object. Missing paths return `404 not_found`.

## Read file content

```
GET /v1/agents/{agentId}/users/{subjectId}/files/content
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "https://api.herm.run/v1/agents/agent_123/users/user_123/files/content?path=reports/q3.md" \
  -H "x-api-key: $HERM_API_KEY"
```

JSON is the default response. Text files use `encoding: "utf-8"`. Binary files
use `encoding: "base64"`.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "object": "file_content",
  "path": "reports/q3.md",
  "type": "file",
  "sizeBytes": 37,
  "mimeType": "text/markdown",
  "encoding": "utf-8",
  "content": "Q3 pipeline grew 18% against plan.",
  "checksumSha256": "4f3c0a1b2d8e9f6a7c5b4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a",
  "createdAt": "2026-08-13T18:00:00.000Z",
  "updatedAt": "2026-08-13T18:05:00.000Z"
}
```

To download bytes instead of JSON, send `Accept: application/octet-stream`.
Directories cannot be downloaded and return `400 validation_error`.

Each file is capped at 40 MiB. Larger objects return `400 validation_error`.

## Write file content

```
PUT /v1/agents/{agentId}/users/{subjectId}/files/content
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PUT \
  "https://api.herm.run/v1/agents/agent_123/users/user_123/files/content" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $HERM_API_KEY" \
  -d '{
    "path": "reports/q3.md",
    "encoding": "utf-8",
    "content": "Q3 pipeline grew 18% against plan.",
    "overwrite": true,
    "createParents": true
  }'
```

| Field           | Type    | Required | Description                                            |
| --------------- | ------- | -------- | ------------------------------------------------------ |
| `path`          | string  | Yes      | Workspace-relative destination path.                   |
| `encoding`      | string  | No       | `utf-8` (default) or `base64`.                         |
| `content`       | string  | Yes      | File body in the chosen encoding.                      |
| `overwrite`     | boolean | No       | Replace an existing file. Defaults to `false`.         |
| `createParents` | boolean | No       | Create missing parent directories. Defaults to `true`. |
| `mimeType`      | string  | No       | Optional MIME type hint stored with the file.          |

The first write for a subject creates the volume if needed. Writing a path that
already exists with `overwrite: false` returns `409 conflict`. Writing a
directory path, a reserved `memories/` or `skills/` path, or a body over 40 MiB
returns `400 validation_error`.

The response is the updated `file` metadata object.

### Upload binary files

For images, PDFs, and other binaries, send `encoding: "base64"` or use
multipart:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PUT \
  "https://api.herm.run/v1/agents/agent_123/users/user_123/files/content?path=uploads/logo.png&overwrite=true" \
  -H "Content-Type: application/octet-stream" \
  -H "x-api-key: $HERM_API_KEY" \
  --data-binary @logo.png
```

Query `path` and `overwrite` apply to octet-stream uploads. `createParents`
defaults to `true`.

## Create a directory

```
POST /v1/agents/{agentId}/users/{subjectId}/files/mkdir
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST \
  "https://api.herm.run/v1/agents/agent_123/users/user_123/files/mkdir" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $HERM_API_KEY" \
  -d '{
    "path": "reports/charts",
    "createParents": true
  }'
```

Creating a directory that already exists is idempotent and returns the existing
directory metadata. Creating a directory over a file path returns `409 conflict`.

## Move or rename

```
POST /v1/agents/{agentId}/users/{subjectId}/files/move
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST \
  "https://api.herm.run/v1/agents/agent_123/users/user_123/files/move" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $HERM_API_KEY" \
  -d '{
    "from": "reports/draft.md",
    "to": "reports/q3.md",
    "overwrite": false
  }'
```

Move works for files and directories. Parent directories of `to` are created
when missing. `overwrite: false` (the default) returns `409 conflict` when the
destination exists. Reserved `memories/` and `skills/` paths cannot be moved.

## Copy

```
POST /v1/agents/{agentId}/users/{subjectId}/files/copy
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST \
  "https://api.herm.run/v1/agents/agent_123/users/user_123/files/copy" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $HERM_API_KEY" \
  -d '{
    "from": "reports/q3.md",
    "to": "archive/q3-copy.md",
    "overwrite": false
  }'
```

Copying a directory copies its contents recursively. The source is left in
place. Destination limits match write: 40 MiB per file and the subject's volume
ceiling.

## Delete

```
DELETE /v1/agents/{agentId}/users/{subjectId}/files
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X DELETE \
  "https://api.herm.run/v1/agents/agent_123/users/user_123/files?path=reports/q3.md" \
  -H "x-api-key: $HERM_API_KEY"
```

| Query       | Type    | Required | Description                                                    |
| ----------- | ------- | -------- | -------------------------------------------------------------- |
| `path`      | string  | Yes      | File or directory to delete.                                   |
| `recursive` | boolean | No       | Required to delete a non-empty directory. Defaults to `false`. |

Deleting a missing path returns `404 not_found`. Deleting a non-empty directory
without `recursive=true` returns `409 conflict`. Reserved memory and skill
paths cannot be deleted through this endpoint.

Response:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "object": "file.deleted",
  "path": "reports/q3.md",
  "deleted": true
}
```

## Search files

```
GET /v1/agents/{agentId}/users/{subjectId}/files/search
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "https://api.herm.run/v1/agents/agent_123/users/user_123/files/search?query=pipeline&path=reports&maxResults=20" \
  -H "x-api-key: $HERM_API_KEY"
```

| Query        | Type    | Required | Description                                                   |
| ------------ | ------- | -------- | ------------------------------------------------------------- |
| `query`      | string  | Yes      | Substring matched against file names and UTF-8 text contents. |
| `path`       | string  | No       | Directory to search within. Defaults to the workspace root.   |
| `maxResults` | integer | No       | 1–100. Defaults to `20`.                                      |

Search skips binary files. Each match includes the path, a short excerpt, and
whether the query hit the name, the contents, or both.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "object": "list",
  "query": "pipeline",
  "data": [
    {
      "object": "file_match",
      "path": "reports/q3.md",
      "name": "q3.md",
      "match": "content",
      "excerpt": "Q3 pipeline grew 18% against plan."
    }
  ]
}
```

## Get volume usage

```
GET /v1/agents/{agentId}/users/{subjectId}/volume
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "https://api.herm.run/v1/agents/agent_123/users/user_123/volume" \
  -H "x-api-key: $HERM_API_KEY"
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "object": "sandbox.volume",
  "agentId": "agent_123",
  "subjectId": "user_123",
  "status": "ready",
  "sizeBytes": 536870912,
  "usedBytes": 12058624,
  "availableBytes": 524812288,
  "maxSizeBytes": 4294967296,
  "createdAt": "2026-08-13T17:58:00.000Z",
  "updatedAt": "2026-08-13T18:05:00.000Z"
}
```

| `status`      | Meaning                                                                |
| ------------- | ---------------------------------------------------------------------- |
| `none`        | No volume exists yet. The next write or session creates one.           |
| `warming`     | Herm is attaching or expanding the volume. Retry shortly.              |
| `ready`       | The volume can be read and written.                                    |
| `unavailable` | The volume exists but cannot be reached right now. Files are not lost. |

Volumes start at 512 MiB and expand automatically toward 4 GiB as usage
approaches capacity. See [Volume capacity](/guides/files#volume-capacity).

## Visibility during sessions

Writes from this API land on the same volume the agent mounts. A running turn
may not observe a write until the next turn. Prefer writing before
[`POST /v1/sessions`](/api-reference/sessions#create-a-session), after the
session is `idle`, or while using
[`POST /v1/sandboxes/warm`](/api-reference/sessions#prewarm-an-agent) so the
sandbox is already attached.

File operations count against the organization API key request ceiling, not the
per-subject conversation or message allowances.

## Endpoint summary

| Method   | Path                                                   | Description                          |
| -------- | ------------------------------------------------------ | ------------------------------------ |
| `GET`    | `/v1/agents/{agentId}/users/{subjectId}/files`         | List files and directories.          |
| `GET`    | `/v1/agents/{agentId}/users/{subjectId}/files/stat`    | Get file or directory metadata.      |
| `GET`    | `/v1/agents/{agentId}/users/{subjectId}/files/content` | Read file content as JSON or bytes.  |
| `PUT`    | `/v1/agents/{agentId}/users/{subjectId}/files/content` | Create or replace a file.            |
| `POST`   | `/v1/agents/{agentId}/users/{subjectId}/files/mkdir`   | Create a directory.                  |
| `POST`   | `/v1/agents/{agentId}/users/{subjectId}/files/move`    | Move or rename a path.               |
| `POST`   | `/v1/agents/{agentId}/users/{subjectId}/files/copy`    | Copy a file or directory.            |
| `DELETE` | `/v1/agents/{agentId}/users/{subjectId}/files`         | Delete a file or directory.          |
| `GET`    | `/v1/agents/{agentId}/users/{subjectId}/files/search`  | Search file names and text contents. |
| `GET`    | `/v1/agents/{agentId}/users/{subjectId}/volume`        | Read volume usage and status.        |

## Errors

| Status | Error              | When                                                                                              |
| ------ | ------------------ | ------------------------------------------------------------------------------------------------- |
| `400`  | `validation_error` | The path, encoding, body, or size is invalid, or the path is reserved.                            |
| `404`  | `not_found`        | The agent, file, or directory does not exist.                                                     |
| `409`  | `conflict`         | The destination exists and `overwrite` is false, or a non-empty directory needs `recursive=true`. |
| `409`  | `invalid_state`    | The volume is expanding or otherwise not writable. Retry after `Retry-After`.                     |
| `429`  | `rate_limited`     | The organization API key request ceiling is exhausted.                                            |
