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

# Files

> Browse, read, write, move, delete, and download whole folders on the instance's disk — straight from the instance URL.

Files live on the instance's disk. A file's absolute `path` is its identity: there are no file ids, so the path you list is the path you read, write, move, or delete. You write a file with one call, attach the returned `path` to [a message](/agents-api/chat), and download anything the agent produces by path.

The base URL is your instance URL, `https://{instanceId}.agent37.app`, with the same `sk_live_` key as every other call, sent as the `X-Agent37-Key` header. This page uses `https://ab12cd34ef.agent37.app`. The agent's workspace — where it reads and writes by default — is `/home/user/.agent37-gateway/workspace`, and that is the default directory for a list with no `path`.

These calls are not jailed to the workspace. The `sk_live_` key is the instance root: any path the key can reach on the instance's filesystem is fair game, with `~` expanding to the agent's home. Treat the key accordingly.

<Note>
  Every timestamp here is `modified`, the file's mtime in **epoch milliseconds** — the Agent API convention (the Hosting API uses seconds). It is a number, not an ISO string.
</Note>

## The file entry

List responses and every write return the same `FileEntry` shape, so the `path` you get back from a write is ready to use on the next call.

<ResponseField name="name" type="string">
  The basename, e.g. `leads.csv`.
</ResponseField>

<ResponseField name="path" type="string">
  The resolved absolute path on the instance. This is the identity you pass to every other call and to `files` on [`POST /v1/responses`](/agents-api/chat).
</ResponseField>

<ResponseField name="type" type="string">
  `file`, `directory`, `symlink`, or `other` (sockets, devices, FIFOs).
</ResponseField>

<ResponseField name="size" type="integer | null">
  Size in bytes; `null` for directories.
</ResponseField>

<ResponseField name="modified" type="number">
  Last-modified time (mtime) in epoch milliseconds.
</ResponseField>

<ResponseField name="hidden" type="boolean">
  `true` when the name starts with `.`.
</ResponseField>

## List a directory

`GET /v1/files` lists one directory level. Omit `path` to list the agent's workspace; pass an absolute path or a `~/` path to list anywhere the key can reach. Entries are sorted directories first, then by name case-insensitively.

<ParamField query="path" type="string">
  The directory to list. Optional; defaults to the agent's workspace, `/home/user/.agent37-gateway/workspace`. Accepts absolute and `~/` paths. A path that exists but is not a directory returns `400 not_a_directory`.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -G https://ab12cd34ef.agent37.app/v1/files \
    -H "X-Agent37-Key: sk_live_..." \
    --data-urlencode "path=~/.agent37-gateway/workspace"
  ```

  ```python python theme={null}
  import requests

  listing = requests.get(
      "https://ab12cd34ef.agent37.app/v1/files",
      headers={"X-Agent37-Key": "sk_live_..."},
      params={"path": "~/.agent37-gateway/workspace"},
  ).json()
  ```

  ```javascript node theme={null}
  const listing = await (await fetch(
    "https://ab12cd34ef.agent37.app/v1/files?" +
      new URLSearchParams({ path: "~/.agent37-gateway/workspace" }),
    { headers: { "X-Agent37-Key": "sk_live_..." } },
  )).json();
  ```

  ```json response theme={null}
  {
    "path": "/home/user/.agent37-gateway/workspace",
    "parentPath": "/home/user/.agent37-gateway",
    "entries": [
      {
        "name": "reports",
        "path": "/home/user/.agent37-gateway/workspace/reports",
        "type": "directory",
        "size": null,
        "modified": 1781049600000,
        "hidden": false
      },
      {
        "name": "leads.csv",
        "path": "/home/user/.agent37-gateway/workspace/leads.csv",
        "type": "file",
        "size": 18244,
        "modified": 1781049642000,
        "hidden": false
      }
    ],
    "truncated": false
  }
  ```
</CodeGroup>

<ResponseField name="path" type="string">
  The resolved absolute path of the directory you listed.
</ResponseField>

<ResponseField name="parentPath" type="string | null">
  The parent directory's absolute path, or `null` at the filesystem root.
</ResponseField>

<ResponseField name="entries" type="array">
  The directory's immediate children as [`FileEntry`](#the-file-entry) objects. One level only — this never recurses.
</ResponseField>

<ResponseField name="truncated" type="boolean">
  `true` when the directory holds more than 1000 entries; only the first 1000 (after sorting) are returned.
</ResponseField>

## Read, preview, or download a file

`GET /v1/files/content?path=…` streams a file off the instance — typically one the agent told you it wrote. Any size; the 512 KB [exec](/agents-api/exec) output cap does not apply here. The `Content-Type` is set from the file extension.

<ParamField query="path" type="string" required>
  The file to read. Accepts absolute and `~/` paths. A missing or empty `path`, or a path that is not a regular file, returns `400 validation_error`; no file at the path returns `404 file_not_found`.
</ParamField>

<ParamField query="disposition" type="string" default="attachment">
  `attachment` sends `Content-Disposition: attachment` so a browser downloads the file. `inline` sends `Content-Disposition: inline` so a browser renders it (useful for previews).
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -G https://ab12cd34ef.agent37.app/v1/files/content \
    -H "X-Agent37-Key: sk_live_..." \
    --data-urlencode "path=~/.agent37-gateway/workspace/reports/ev-makers-memo.md" \
    -o ev-makers-memo.md
  ```

  ```python python theme={null}
  import requests

  r = requests.get(
      "https://ab12cd34ef.agent37.app/v1/files/content",
      headers={"X-Agent37-Key": "sk_live_..."},
      params={"path": "~/.agent37-gateway/workspace/reports/ev-makers-memo.md"},
  )
  open("ev-makers-memo.md", "wb").write(r.content)
  ```

  ```javascript node theme={null}
  import fs from "node:fs";

  const res = await fetch(
    "https://ab12cd34ef.agent37.app/v1/files/content?" +
      new URLSearchParams({
        path: "~/.agent37-gateway/workspace/reports/ev-makers-memo.md",
      }),
    { headers: { "X-Agent37-Key": "sk_live_..." } },
  );
  await fs.promises.writeFile(
    "ev-makers-memo.md",
    Buffer.from(await res.arrayBuffer()),
  );
  ```
</CodeGroup>

<Warning>
  Serving an agent-produced file `inline` runs it on **your** origin. HTML, SVG, and similar can execute scripts in the page that opens them, so an instance whose agent writes attacker-controlled content can run code against your users. Render untrusted files in a sandboxed frame (`<iframe sandbox>`) on an isolated origin, or force a download with `disposition=attachment`.
</Warning>

## Download a folder

`GET /v1/files/archive?path=…` streams a whole directory as a gzipped tar (`.tar.gz`) — the one call to pull a tree instead of walking it file by file. The archive is built on the fly and streamed, so any size works and the 512 KB [exec](/agents-api/exec) output cap does not apply. It unpacks to a single top-level folder named after the directory you packed.

<ParamField query="path" type="string">
  The directory to archive. Optional; defaults to the agent's workspace, `/home/user/.agent37-gateway/workspace`. Accepts absolute and `~/` paths. No directory at the path returns `404 file_not_found`; a path that exists but is not a directory returns `400 not_a_directory`.
</ParamField>

The response is `Content-Type: application/gzip` and a `Content-Disposition: attachment` whose download filename is the packed directory's name plus `.tar.gz` (characters outside `A–Z a–z 0–9 . _ -` and space are stripped, and an empty result falls back to `archive`). Symlinks are stored as links, not followed, so the archive never inlines a link target's bytes.

<CodeGroup>
  ```bash curl theme={null}
  curl -G https://ab12cd34ef.agent37.app/v1/files/archive \
    -H "X-Agent37-Key: sk_live_..." \
    --data-urlencode "path=/home/user/.agent37-gateway/workspace/reports" \
    -o reports.tar.gz
  ```

  ```python python theme={null}
  import requests

  r = requests.get(
      "https://ab12cd34ef.agent37.app/v1/files/archive",
      headers={"X-Agent37-Key": "sk_live_..."},
      params={"path": "/home/user/.agent37-gateway/workspace/reports"},
  )
  open("reports.tar.gz", "wb").write(r.content)
  ```

  ```javascript node theme={null}
  import fs from "node:fs";

  const res = await fetch(
    "https://ab12cd34ef.agent37.app/v1/files/archive?" +
      new URLSearchParams({
        path: "/home/user/.agent37-gateway/workspace/reports",
      }),
    { headers: { "X-Agent37-Key": "sk_live_..." } },
  );
  await fs.promises.writeFile("reports.tar.gz", Buffer.from(await res.arrayBuffer()));
  ```
</CodeGroup>

Expand it with `tar -xzf reports.tar.gz`. There is no folder-upload counterpart: to upload a tree, recreate it with per-file `PUT /v1/files/content` calls — each creates any missing parent directories (`mkdir -p`).

## Write a file

`PUT /v1/files/content?path=…` writes the **raw request body** to `path` — this is the one call for create, overwrite, edit, and upload. It is not multipart: the body is the file's exact bytes. Missing parent directories are created (`mkdir -p`). The response is the written file's [`FileEntry`](#the-file-entry).

<ParamField query="path" type="string" required>
  Where to write. Accepts absolute and `~/` paths; parent directories are created as needed. A missing or empty `path` returns `400 validation_error`.
</ParamField>

<ParamField query="overwrite" type="boolean" default="true">
  `true` replaces an existing file. `false` makes the write fail with `409 file_exists` if a file is already at `path`.
</ParamField>

<ParamField header="X-Expected-Mtime" type="integer">
  Optional optimistic-concurrency guard, epoch milliseconds. When the file exists and its `modified` differs from this value, the write fails with `412 modified` — someone changed it since you read it. Ignored when the file does not exist (the write is treated as a create).
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X PUT "https://ab12cd34ef.agent37.app/v1/files/content?path=/home/user/.agent37-gateway/workspace/leads.csv" \
    -H "X-Agent37-Key: sk_live_..." \
    -H "Content-Type: text/csv" \
    --data-binary @leads.csv
  ```

  ```python python theme={null}
  import requests

  entry = requests.put(
      "https://ab12cd34ef.agent37.app/v1/files/content",
      headers={"X-Agent37-Key": "sk_live_..."},
      params={"path": "/home/user/.agent37-gateway/workspace/leads.csv"},
      data=open("leads.csv", "rb"),
  ).json()
  ```

  ```javascript node theme={null}
  import fs from "node:fs";

  const entry = await (await fetch(
    "https://ab12cd34ef.agent37.app/v1/files/content?" +
      new URLSearchParams({
        path: "/home/user/.agent37-gateway/workspace/leads.csv",
      }),
    {
      method: "PUT",
      headers: { "X-Agent37-Key": "sk_live_..." },
      body: await fs.promises.readFile("leads.csv"),
    },
  )).json();
  ```

  ```json response theme={null}
  {
    "name": "leads.csv",
    "path": "/home/user/.agent37-gateway/workspace/leads.csv",
    "type": "file",
    "size": 18244,
    "modified": 1781049642000,
    "hidden": false
  }
  ```
</CodeGroup>

<Tip>
  **Attach a file to a turn.** Write the bytes, then pass the returned `path` in the `files` array on [`POST /v1/responses`](/agents-api/chat):

  ```bash theme={null}
  curl https://ab12cd34ef.agent37.app/v1/responses \
    -H "X-Agent37-Key: sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "input": "Summarize the attached spreadsheet.",
      "files": ["/home/user/.agent37-gateway/workspace/leads.csv"]
    }'
  ```

  Each entry must name an existing file on the instance, or the call returns `400 validation_error`.
</Tip>

## Delete a file or directory

`DELETE /v1/files?path=…` removes the path recursively and by force, like `rm -rf` — a directory and everything under it goes in one call. There is no confirmation and no guard, so a wrong `path` is unrecoverable. A symlink is removed itself, not followed. The response is `{ "ok": true }`.

<ParamField query="path" type="string" required>
  The file or directory to delete. Accepts absolute and `~/` paths. A missing or empty `path` returns `400 validation_error`.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X DELETE -G https://ab12cd34ef.agent37.app/v1/files \
    -H "X-Agent37-Key: sk_live_..." \
    --data-urlencode "path=/home/user/.agent37-gateway/workspace/reports"
  ```

  ```python python theme={null}
  import requests

  requests.delete(
      "https://ab12cd34ef.agent37.app/v1/files",
      headers={"X-Agent37-Key": "sk_live_..."},
      params={"path": "/home/user/.agent37-gateway/workspace/reports"},
  )
  ```

  ```javascript node theme={null}
  await fetch(
    "https://ab12cd34ef.agent37.app/v1/files?" +
      new URLSearchParams({
        path: "/home/user/.agent37-gateway/workspace/reports",
      }),
    { method: "DELETE", headers: { "X-Agent37-Key": "sk_live_..." } },
  );
  ```

  ```json response theme={null}
  { "ok": true }
  ```
</CodeGroup>

<Warning>
  Delete is recursive and unguarded. It removes whatever the key can reach, including directories full of files, with no undo. Double-check `path` before you send it.
</Warning>

## Rename or move a file

`PATCH /v1/files` renames or moves a path with `fs.rename`, taking a body of `{ "from", "to" }`. The OS decides the edge cases — overwriting an existing `to`, moving into a directory, crossing devices — so behavior matches a shell `mv`. The response is the [`FileEntry`](#the-file-entry) of the new path.

<ParamField body="from" type="string" required>
  The current path. Accepts absolute and `~/` paths. Empty returns `400 validation_error`.
</ParamField>

<ParamField body="to" type="string" required>
  The new path. Accepts absolute and `~/` paths. Empty returns `400 validation_error`.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X PATCH https://ab12cd34ef.agent37.app/v1/files \
    -H "X-Agent37-Key: sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "from": "/home/user/.agent37-gateway/workspace/leads.csv",
      "to": "/home/user/.agent37-gateway/workspace/archive/leads.csv"
    }'
  ```

  ```python python theme={null}
  import requests

  entry = requests.patch(
      "https://ab12cd34ef.agent37.app/v1/files",
      headers={"X-Agent37-Key": "sk_live_..."},
      json={
          "from": "/home/user/.agent37-gateway/workspace/leads.csv",
          "to": "/home/user/.agent37-gateway/workspace/archive/leads.csv",
      },
  ).json()
  ```

  ```javascript node theme={null}
  const entry = await (await fetch("https://ab12cd34ef.agent37.app/v1/files", {
    method: "PATCH",
    headers: {
      "X-Agent37-Key": "sk_live_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: "/home/user/.agent37-gateway/workspace/leads.csv",
      to: "/home/user/.agent37-gateway/workspace/archive/leads.csv",
    }),
  })).json();
  ```

  ```json response theme={null}
  {
    "name": "leads.csv",
    "path": "/home/user/.agent37-gateway/workspace/archive/leads.csv",
    "type": "file",
    "size": 18244,
    "modified": 1781049642000,
    "hidden": false
  }
  ```
</CodeGroup>

## Create a directory

`POST /v1/files/dir?path=…` creates a directory and any missing parents (`mkdir -p`). It is idempotent: a path that already exists returns its [`FileEntry`](#the-file-entry) rather than erroring.

<ParamField query="path" type="string" required>
  The directory to create. Accepts absolute and `~/` paths; parents are created as needed. A missing or empty `path` returns `400 validation_error`.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST -G https://ab12cd34ef.agent37.app/v1/files/dir \
    -H "X-Agent37-Key: sk_live_..." \
    --data-urlencode "path=/home/user/.agent37-gateway/workspace/archive"
  ```

  ```python python theme={null}
  import requests

  entry = requests.post(
      "https://ab12cd34ef.agent37.app/v1/files/dir",
      headers={"X-Agent37-Key": "sk_live_..."},
      params={"path": "/home/user/.agent37-gateway/workspace/archive"},
  ).json()
  ```

  ```javascript node theme={null}
  const entry = await (await fetch(
    "https://ab12cd34ef.agent37.app/v1/files/dir?" +
      new URLSearchParams({
        path: "/home/user/.agent37-gateway/workspace/archive",
      }),
    { method: "POST", headers: { "X-Agent37-Key": "sk_live_..." } },
  )).json();
  ```

  ```json response theme={null}
  {
    "name": "archive",
    "path": "/home/user/.agent37-gateway/workspace/archive",
    "type": "directory",
    "size": null,
    "modified": 1781049600000,
    "hidden": false
  }
  ```
</CodeGroup>

## The loop

The common cycle is write, attach, fetch:

1. `PUT /v1/files/content?path=…` with the input bytes; keep the returned `path`.
2. `POST /v1/responses` with your `input` and that path in `files`.
3. When the agent replies that it wrote a file, `GET /v1/files/content?path=…` to fetch it — or `GET /v1/files` to browse what it left behind.
