# Filesystem

> Read, write, move and delete files; directories, glob, full-text search and directory forks.

Source: https://useprimitives.com/docs/api/filesystem

These routes apply to a primitive of type `fs`. See [Filesystem](/docs/primitives/filesystem) for how paths, globs, search and directory forks work. Calling one of them on a primitive of another type answers `400 wrong_type`.

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/v1/primitives/{id}/files/{path}` | Read a file, list a directory, or stat either |
| `PUT` | `/v1/primitives/{id}/files/{path}` | Write a file |
| `DELETE` | `/v1/primitives/{id}/files/{path}` | Delete a file or directory |
| `POST` | `/v1/primitives/{id}/move` | Move or rename a file or directory |
| `POST` | `/v1/primitives/{id}/mkdir` | Create a directory |
| `GET` | `/v1/primitives/{id}/glob` | Find paths matching a glob pattern |
| `GET` | `/v1/primitives/{id}/search` | Full-text search over text files |
| `POST` | `/v1/primitives/{id}/forks` | Fork a directory inside the filesystem |

`{path}` is the rest of the URL, so `/files/repo/src/main.rs` addresses `/repo/src/main.rs`. `GET /v1/primitives/{id}/files` with no path addresses the root. Paths are normalized: a path without a leading `/` is taken from the root, one trailing `/` is ignored, `.`, `..` and empty segments are `400 invalid_request`, and paths are at most 1,024 bytes.

Every route answers `404 not_found` for a primitive that doesn't exist or is outside the API key's group, and `423 paused` while the primitive is paused. Timestamps are milliseconds since the Unix epoch. See [Errors](/docs/api/errors) for the full list of codes.

## Read a file

`GET /v1/primitives/{id}/files/{path}`

On a file, returns its raw bytes. The `Content-Type` is `text/plain; charset=utf-8` when the contents are valid UTF-8, otherwise `application/octet-stream`. These headers carry the file's metadata:

| Header | Description |
| --- | --- |
| `X-Primitives-Hash` | BLAKE3 hash of the contents, hex |
| `X-Primitives-Size` | Size in bytes |
| `X-Primitives-Mtime` | Last modification, ms since the epoch |

On a directory, the same request lists it; see [List a directory](#list-a-directory). A path that doesn't exist is `404 not_found`.

Key tool: `fs_read`.

```bash
curl https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/files/repo/plan.md \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
X-Primitives-Hash: 9f2c4a1e7b3d5f80c6a2e4b1d9f7c3a5e8b0d2f4a6c8e1b3d5f7a9c2e4b6d8f0
X-Primitives-Size: 31
X-Primitives-Mtime: 1760486400000

# Plan

1. Write the parser.
```

## List a directory

`GET /v1/primitives/{id}/files/{path}?list=1`

Lists the entries of a directory as FileInfo, directories first, then by path. Add `recursive=1` to list every descendant instead of only the direct children. A `GET` on a directory lists it even without `list`. With `list` on a file, the call answers `409 fs_conflict`.

The response uses the [list envelope](/docs/api#pagination) but always comes in one page: `next_cursor` is `null`.

Key tool: `fs_read`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `list` | flag | List the directory. A flag is on for `1`, `true`, `yes` or an empty value. |
| `recursive` | flag | List all descendants, not only direct children. |

```bash
curl "https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/files/repo?list=1&recursive=1" \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{
  "data": [
    { "path": "/repo/src", "kind": "dir", "size": 0, "hash": null, "mode": 493, "mtime": 1760486400000 },
    { "path": "/repo/plan.md", "kind": "file", "size": 31, "hash": "9f2c4a1e7b3d5f80c6a2e4b1d9f7c3a5e8b0d2f4a6c8e1b3d5f7a9c2e4b6d8f0", "mode": 420, "mtime": 1760486400000 },
    { "path": "/repo/src/main.rs", "kind": "file", "size": 1204, "hash": "3b7e9a1c5d2f4806e8a0c2b4d6f8a1c3e5b7d9f0a2c4e6b8d1f3a5c7e9b2d4f6", "mode": 420, "mtime": 1760486460000 }
  ],
  "next_cursor": null
}
```

`mode` is a number: `420` is `0644` and `493` is `0755`.

## Get file info

`GET /v1/primitives/{id}/files/{path}?stat=1`

Returns the FileInfo of a file or directory without its contents. `stat` takes precedence over `list`. The root reports `mode` `493` and `mtime` `0`.

Key tool: `fs_read`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `stat` | flag | Return metadata only. A flag is on for `1`, `true`, `yes` or an empty value. |

```bash
curl "https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/files/repo/plan.md?stat=1" \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{
  "path": "/repo/plan.md",
  "kind": "file",
  "size": 31,
  "hash": "9f2c4a1e7b3d5f80c6a2e4b1d9f7c3a5e8b0d2f4a6c8e1b3d5f7a9c2e4b6d8f0",
  "mode": 420,
  "mtime": 1760486400000
}
```

## Write a file

`PUT /v1/primitives/{id}/files/{path}`

Creates or replaces a file. The raw request body is the contents. Missing parent directories are created. Returns the file's FileInfo.

The mode defaults to the existing file's, else `0644`. Files are at most 32 MiB, and a filesystem holds at most 1 GiB of file contents; over either is `413 payload_too_large`. Writing over a directory, or to `/`, is `409 fs_conflict`. A mode that isn't octal, or is above `7777`, is `400 invalid_request`.

Key tool: `fs_write`.

**Headers**

| Parameter | Type | Description |
| --- | --- | --- |
| `X-Primitives-Mode` | string | Permission bits in octal, such as `644`, `0755` or `0o755`. |

**Body**

Required. The file's contents, as raw bytes.

```bash
curl -X PUT https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/files/repo/run.sh \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "X-Primitives-Mode: 755" \
  --data-binary @run.sh
```

```json
{
  "path": "/repo/run.sh",
  "kind": "file",
  "size": 88,
  "hash": "c1e3a5b7d9f2a4c6e8b0d1f3a5c7e9b2d4f6a8c0e2b4d6f8a1c3e5b7d9f0a2c4",
  "mode": 493,
  "mtime": 1760486520000
}
```

## Delete a file or directory

`DELETE /v1/primitives/{id}/files/{path}`

Deletes a file, or a directory. A non-empty directory needs `recursive=1`; without it the call answers `409 fs_conflict`. Deleting `/` is `400 invalid_request`, and a path that doesn't exist is `404 not_found`.

Key tool: `fs_write`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `recursive` | flag | Delete a non-empty directory and everything under it. |

```bash
curl -X DELETE "https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/files/scratch?recursive=1" \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

Returns `204` with no body.

## Move a file or directory

`POST /v1/primitives/{id}/move`

Moves or renames a file, or a directory with its whole subtree. Missing parent directories of `to` are created. Returns the FileInfo at the destination.

If `to` exists, the call answers `409 fs_conflict` unless `overwrite` is `true`. Even then, a file can only replace a file and a directory only an empty directory. Moving `/`, replacing it, or moving a directory into itself is `400 invalid_request`. A missing `from` is `404 not_found`.

Key tool: `fs_write`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `from` | string | Required. The path to move. |
| `to` | string | Required. The new path. |
| `overwrite` | boolean | Replace an existing file, or an empty directory, at `to`. Default `false`. |

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/move \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"from": "/repo/plan.md", "to": "/repo/docs/plan.md"}'
```

```json
{
  "path": "/repo/docs/plan.md",
  "kind": "file",
  "size": 31,
  "hash": "9f2c4a1e7b3d5f80c6a2e4b1d9f7c3a5e8b0d2f4a6c8e1b3d5f7a9c2e4b6d8f0",
  "mode": 420,
  "mtime": 1760486400000
}
```

## Create a directory

`POST /v1/primitives/{id}/mkdir`

Creates a directory and any missing parents, with mode `0755`. It's idempotent: an existing directory is returned as it is, also with `201`. A file at that path is `409 fs_conflict`.

Key tool: `fs_write`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `path` | string | Required. The directory to create. |

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/mkdir \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path": "/repo/docs"}'
```

Returns `201` with the FileInfo:

```json
{
  "path": "/repo/docs",
  "kind": "dir",
  "size": 0,
  "hash": null,
  "mode": 493,
  "mtime": 1760486580000
}
```

## Glob

`GET /v1/primitives/{id}/glob`

Returns the paths of files and directories that match a glob pattern, in path order. Patterns match absolute paths; a relative pattern is anchored at `/`. `*` stays within one path segment, `**` crosses segments, and `?`, `[ab]` and `{a,b}` work as usual. An empty or invalid pattern is `400 invalid_request`.

Returns `{ "data": [paths] }` with no `next_cursor`.

Key tool: `fs_search`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `pattern` | string | Required. The glob pattern, such as `/repo/src/**/*.rs`. |
| `limit` | integer | Maximum paths to return. Default 50, max 1,000. |

```bash
curl "https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/glob?pattern=/repo/**/*.md" \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{
  "data": ["/repo/README.md", "/repo/docs/plan.md"]
}
```

## Search

`GET /v1/primitives/{id}/search`

Full-text search over text files up to 1 MiB, best matches first. Every word in `q` must match. Operators and punctuation are treated as plain text, and a query with no letters or digits returns no results. In each `snippet`, matched terms are wrapped in `**` and cut text is marked with `…`.

Returns `{ "data": [SearchHit] }` with no `next_cursor`.

Key tool: `fs_search`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `q` | string | Required. The words to search for. |
| `path` | string | Only search this file or directory and what's under it. Default `/`. |
| `limit` | integer | Maximum hits to return. Default 50, max 1,000. |

```bash
curl "https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/search?q=lease%20expired&path=/repo" \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{
  "data": [
    { "path": "/repo/src/queue.rs", "snippet": "…the job returns **lease** **expired** when the worker…" }
  ]
}
```

## Fork a directory

`POST /v1/primitives/{id}/forks`

Copies a directory and its subtree to a new path inside the same filesystem. Files over 64 KiB share their contents with the source instead of being copied.

`to` defaults to `<from>-fork-<6 chars>`. An existing `to` is `409 fs_conflict`. Forking `/`, forking a file, or a `to` inside `from` is `400 invalid_request`. A missing `from` is `404 not_found`. To copy a whole filesystem into a new primitive, use [Fork a primitive](/docs/api/primitives) instead.

Key tool: `fs_fork`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `from` | string | Required. The directory to fork. |
| `to` | string | The new directory. Default `<from>-fork-<6 chars>`. |

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/forks \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"from": "/repo"}'
```

Returns `201` with the ForkResult:

```json
{
  "path": "/repo-fork-x1y2z3",
  "files": 128
}
```

## Shapes

| Shape | Fields |
| --- | --- |
| FileInfo | `{ path, kind, size, hash, mode, mtime }`. `path` is absolute and normalized. `kind` is `"file"` or `"dir"`. `size` is in bytes, `0` for a directory. `hash` is the BLAKE3 hash of a file's contents in hex, or `null` for a directory. `mode` is the permission bits as a number (`420` is `0644`). `mtime` is the last modification |
| SearchHit | `{ path, snippet }`. `snippet` is an excerpt around the match, with matched terms wrapped in `**` |
| ForkResult | `{ path, files }`. `path` is the new directory; `files` is the number of files in it |

Scoped API keys need the tool named in each section on their allowlist; see [Tool allowlists](/docs/api/authentication#tool-allowlists). For creating, snapshotting, forking and pausing the filesystem itself, see [Primitives](/docs/api/primitives). See [Limits](/docs/api/limits) for how operations are counted.
