# Filesystem

> Read, write, list, move, copy and watch files in a sandbox without running a command.

The filesystem routes act on the disk of the sandbox directly. Each one wakes a sandbox that is not running.

```http
/v1/sandboxes/{name}/fs/{operation}
```

## Paths

Every route takes a `path`. A relative path is resolved against `workingDir`, which must be absolute and defaults to `/`. So `path=app/main.py&workingDir=/root` and `path=/root/app/main.py` name the same file.

## Read a file

```http
GET /fs/read?path=/root/app/main.py
```

Answers the bytes of the file as `application/octet-stream`, streamed.

## Write a file

```http
PUT /fs/write?path=/root/app/main.py
```

The body is the new content of the file, up to 16 MiB. A file that exists is replaced.

| Query   | About                                           |
| ------- | ----------------------------------------------- |
| `mkdir` | `true` to create missing parent directories.    |
| `mode`  | Octal permissions for the file, such as `0755`. |

```bash
curl -X PUT "$API/agent-1/fs/write?path=/root/run.sh&mode=0755&mkdir=true" -H "$AUTH" --data-binary @run.sh
```

```json
{ "path": "/root/run.sh", "size": 214, "mode": "0755" }
```

## List a directory

```http
GET /fs/list?path=/root/app
```

| Query       | About                                                             |
| ----------- | ----------------------------------------------------------------- |
| `recursive` | `true` to list the whole tree. Names are then paths below `path`. |
| `pattern`   | Keep only entries that match this gitignore style pattern.        |

```json
{
  "path": "/root/app",
  "entries": [
    {
      "name": "main.py",
      "path": "/root/app/main.py",
      "type": "file",
      "size": 1290,
      "mode": "0644",
      "modTime": "2026-10-31T09:24:11Z",
      "isDir": false
    }
  ],
  "count": 1
}
```

`type` is `file`, `directory` or `symlink`. Listing a file answers that one file as the only entry.

## Stat a path

```http
GET /fs/stat?path=/root/app
```

Answers one entry, shaped as in a listing, for any path, a directory included.

## Delete

```http
DELETE /fs/delete?path=/root/app&recursive=true
```

Deleting a directory with anything in it needs `recursive=true`. The path and flag can also go in a JSON body: `{ "path", "workingDir", "recursive" }`.

```json
{ "deleted": ["/root/app"], "count": 1 }
```

## Rename and copy

```http
POST /fs/rename
POST /fs/copy
```

```json
{ "source": "/root/app", "dest": "/root/app-old", "recursive": true, "preserveAttrs": false }
```

`workingDir` applies to both paths. `recursive` and `preserveAttrs` matter to copy only: copying a directory needs `recursive`, and `preserveAttrs` keeps the attributes of the source. Rename answers `{ "source", "dest" }`; copy also answers `count` and `totalBytes`.

## Change mode and owner

```http
POST /fs/chmod
POST /fs/chown
```

```json
{ "path": "/root/bin", "mode": "0755", "recursive": true }
{ "path": "/srv/www", "uid": "www-data", "gid": 33, "recursive": true }
```

`uid` and `gid` take a number or a name, and at least one is required. Chmod answers `{ "path", "mode", "count" }`; chown answers `{ "path", "affected", "count" }`.

## Watch for changes

```http
GET /fs/watch    (Upgrade: websocket)
```

Send a subscription as text:

```json
{ "type": "subscribe", "paths": ["/root/app"], "recursive": true }
```

The socket answers `{ "type": "subscribed", "paths": [...] }`, then one message per change:

```json
{
  "type": "event",
  "path": "/root/app/main.py",
  "event": "write",
  "timestamp": "2026-10-31T09:25:02Z",
  "isDir": false,
  "size": 1302
}
```

`event` is `create`, `write`, `remove`, `rename` or `chmod`. One socket follows up to 8 paths; send more subscriptions to add paths. If your client reads too slowly, events are dropped and an `error` message says how many.

## Errors

Filesystem errors name the error number of the sandbox in `code`, and the path:

```json
{
  "error": "stat /root/nope: No such file or directory (os error 2)",
  "code": "ENOENT",
  "path": "/root/nope"
}
```

| Status | Codes                                                                                 |
| ------ | ------------------------------------------------------------------------------------- |
| 400    | `ENOTDIR`, `EISDIR`, `ENOTEMPTY`, `EINVAL`, `EXDEV`, `ENAMETOOLONG`, `ELOOP`, `E2BIG` |
| 403    | `EACCES`, `EPERM`, `EROFS`                                                            |
| 404    | `ENOENT`                                                                              |
| 409    | `EEXIST`, `EBUSY`, `ETXTBSY`                                                          |
| 413    | `EFBIG`, or a write body over 16 MiB                                                  |
| 507    | `ENOSPC`, `EDQUOT`: the disk is full                                                  |
| 500    | Anything else, such as `EIO`                                                          |
