The filesystem routes act on the disk of the sandbox directly. Each one wakes a sandbox that is not running.
/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
GET /fs/read?path=/root/app/main.pyAnswers the bytes of the file as application/octet-stream, streamed.
Write a file
PUT /fs/write?path=/root/app/main.pyThe 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. |
curl -X PUT "$API/agent-1/fs/write?path=/root/run.sh&mode=0755&mkdir=true" -H "$AUTH" --data-binary @run.sh{ "path": "/root/run.sh", "size": 214, "mode": "0755" }List a directory
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. |
{
"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
GET /fs/stat?path=/root/appAnswers one entry, shaped as in a listing, for any path, a directory included.
Delete
DELETE /fs/delete?path=/root/app&recursive=trueDeleting a directory with anything in it needs recursive=true. The path and flag can also go in a JSON body: { "path", "workingDir", "recursive" }.
{ "deleted": ["/root/app"], "count": 1 }Rename and copy
POST /fs/rename
POST /fs/copy{ "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
POST /fs/chmod
POST /fs/chown{ "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
GET /fs/watch (Upgrade: websocket)Send a subscription as text:
{ "type": "subscribe", "paths": ["/root/app"], "recursive": true }The socket answers { "type": "subscribed", "paths": [...] }, then one message per change:
{
"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:
{
"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 |