# File System Operations (/docs/file-system-operations)

File system operations are available through the file system module of a sandbox. Each sandbox has its own isolated filesystem, and operations run through the Daytona API, so your application manages files in a sandbox directly, without executing shell commands inside it.

The file system module covers listing directories, reading file metadata, creating directories, uploading and downloading files, setting permissions, searching file contents, watching directories for changes, and moving, renaming, or deleting files. The sandbox streams uploads to disk and downloads from disk, so transfer size is bounded by sandbox storage, not sandbox memory. Most SDK methods hold file content in your application's memory; to move large files without buffering them, stream them from and to local disk with the SDK, as in [upload large files](#upload-large-files) and [download large files](#download-large-files), or use [presigned URLs](#presigned-urls) with an HTTP client.

The API serves file system requests at the sandbox's toolbox proxy URL, written as `{toolboxProxyUrl}` in the examples. It is the `toolbox_proxy_url` field of the [sandbox](/docs/sandboxes#get-a-sandbox), such as `https://proxy.ash-1.us-east.daytona.app/toolbox`.

Sandbox paths must be absolute, starting with a slash. The SDKs raise `BadRequestError` for a relative path before any request is sent, the CLI rejects it, and the API returns `400 INVALID_FILE_PATH`.

<FileSystemVisual />

## Use cases [#use-cases]

File system operations are used when an application moves or inspects files in a sandbox without a shell.

- **Input files.** An application uploads source code, datasets, or configuration before code runs. Several files upload in one request, and a failed file does not stop the others. Missing parent directories are created, and an existing file at the destination is overwritten.

- **Results and artifacts.** Generated files, reports, and build outputs are downloaded after a run. Several files download in one request, and a missing or unreadable file does not stop the others. The SDK methods hold every downloaded file in memory before they return.

- **Agent file tools.** An agent lists, reads, searches, and edits files through the API as tools. The agent needs no shell commands inside the sandbox to do this.

- **Large transfers.** Large files stream between local disk and the sandbox without being held in the application's memory. Uploads have no request size limit, so free sandbox disk space bounds the upload size. A failed or cancelled SDK download leaves an existing destination file unchanged.

- **Direct transfers for other clients.** A presigned URL lets a browser or another service upload or download one file without an API key. A URL is valid for at most 7 days. Rotating the sandbox signing key revokes every outstanding URL within 30 seconds.

## List files and directories [#list-files-and-directories]

List files and directories. By default, only entries directly inside the directory are listed. A depth option lists nested entries up to 32 levels deep; a listing over 10,000 entries fails, so request a smaller depth for large trees. Symbolic links are listed, not followed.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/workspace/hello.txt", "hello")

# List files in a directory
files = sandbox.fs.list("/home/daytona/workspace")

for file in files.items:
    print(f"Name: {file.name}")
    print(f"Is directory: {file.is_dir}")
    print(f"Size: {file.size}")
    print(f"Modified: {file.modified_at}")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/workspace/hello.txt', 'hello')

// List files in a directory
const files = await sandbox.fs.list('/home/daytona/workspace')

files.items.forEach(file => {
  console.log(`Name: ${file.name}`)
  console.log(`Is directory: ${file.isDir}`)
  console.log(`Size: ${file.size}`)
  console.log(`Modified: ${file.modifiedAt ?? file.modTime}`)
})
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files?path=/home/daytona/workspace' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

## Get directory or file information [#get-directory-or-file-information]

Get metadata for a file or directory. The metadata describes the entry at the path itself: for a symbolic link, it describes the link, not its target.

| **Field**               | **Format**                                                                 |
| ----------------------- | -------------------------------------------------------------------------- |
| **Mode**                | Symbolic, such as `-rw-r--r--`; `d` first for a directory, `L` for a link. |
| **Permissions**         | The four-digit octal form, such as `0644`.                                 |
| **Owner** and **group** | Numeric user and group IDs as strings, such as `1000`, not names.          |

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/workspace/data/file.txt", "hello")

# Get file metadata
info = sandbox.fs.stat("/home/daytona/workspace/data/file.txt")
print(f"Size: {info.size} bytes")
print(f"Modified: {info.modified_at}")
print(f"Mode: {info.mode}")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/workspace/data/file.txt', 'hello')

// Get file metadata
const info = await sandbox.fs.stat('/home/daytona/workspace/data/file.txt')
console.log(`Size: ${info.size} bytes`)
console.log(`Modified: ${info.modifiedAt ?? info.modTime}`)
console.log(`Mode: ${info.mode}`)
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/info?path=/home/daytona/workspace/data/file.txt' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

```json
{
  "name": "file.txt",
  "path": "/home/daytona/workspace/data/file.txt",
  "size": 1024,
  "isDir": false,
  "mode": "-rw-r--r--",
  "permissions": "0644",
  "owner": "1000",
  "group": "1000",
  "modifiedAt": "2026-09-13T09:41:12Z",
  "modTime": "2026-09-13 09:41:12 +0000 UTC"
}
```

</TabItem>
</Tabs>

## Create directories [#create-directories]

Create a directory and any missing parents, with octal mode 755 by default.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

sandbox.fs.mkdir("/home/daytona/workspace/new-dir")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

await sandbox.fs.mkdir('/home/daytona/workspace/new-dir')
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/folder?path=/home/daytona/workspace/new-dir' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

## Upload files [#upload-files]

### Upload a single file [#upload-a-single-file]

Upload a file to a sandbox. Missing parent directories at the destination are created, and an existing file at the destination is overwritten.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

with open("local_file.txt", "rb") as f:
    sandbox.fs.write_bytes("/home/daytona/remote_file.txt", f.read())
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { readFile } from 'node:fs/promises'
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

await sandbox.fs.writeBytes('/home/daytona/remote_file.txt', await readFile('local_file.txt'))
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
# The sandbox side is <sandbox-id>:<absolute path>
daytona sandbox cp local_file.txt {sandboxId}:/home/daytona/remote_file.txt
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- Exactly one of the two arguments must be a sandbox path.
- A sandbox destination that ends with `/` receives the file under its local name.
</Callout>

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/upload?path=/home/daytona/remote_file.txt' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --form 'file=@local_file.txt'
```

</TabItem>
</Tabs>

### Upload multiple files [#upload-multiple-files]

Upload several files in one request. Each file is written independently: missing parent directories are created, existing files are overwritten, and a file that fails does not stop the others. The result lists each file written with its byte count, and an error message for each failure.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

result = sandbox.fs.write_many({
    "/home/daytona/data/file1.txt": b"Content of file 1",
    "/home/daytona/data/file2.txt": b"Content of file 2",
})

# Partial success is reported in the result, not raised
for error in result.errors:
    print(f"Error: {error}")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

const result = await sandbox.fs.writeMany({
  '/home/daytona/data/file1.txt': new TextEncoder().encode('Content of file 1'),
  '/home/daytona/data/file2.txt': new TextEncoder().encode('Content of file 2'),
})

// Partial success is reported in the result, not thrown
result.errors.forEach(error => console.error(`Error: ${error}`))
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/bulk-upload' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --form 'files[0].path=/home/daytona/data/file1.txt' \
  --form 'files[0].file=@file1.txt' \
  --form 'files[1].path=/home/daytona/data/file2.txt' \
  --form 'files[1].file=@file2.txt'
```

```json
{
  "files": [
    { "path": "/home/daytona/data/file1.txt", "bytes": 17 },
    { "path": "/home/daytona/data/file2.txt", "bytes": 17 }
  ],
  "errors": []
}
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- Each `files[i].path` part must come before its `files[i].file` part.
</Callout>

</TabItem>
</Tabs>

### Upload large files [#upload-large-files]

Upload large files from local disk. The SDK streams the file in chunks without holding it in memory, and reports progress as it sends. The sandbox writes the upload to disk as it arrives with no request size limit, so free sandbox disk space bounds the upload size. The CLI streams the file from disk over a [presigned URL](#presigned-urls) it creates.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

def on_progress(sent: int, total: int | None) -> None:
    print(f"Uploaded {sent} of {total} bytes")

sandbox.fs.upload_file("large_dataset.csv", "/home/daytona/workspace/dataset.csv", on_progress=on_progress)
```

| **Parameter** | **Type**                   | **Description**                                                                                              |
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `local_path`  | str \| os.PathLike\[str\]  | Sets the local file to upload. This parameter is required. |
| `remote_path` | str                      | Sets the destination path in the sandbox. This parameter is required. |
| `on_progress` | TransferProgressCallback | Reports upload progress. The callback receives the bytes sent so far and the total file size in bytes. Exceptions raised inside the callback are ignored. |
| `cancel`      | TransferCancelSignal     | Stops the upload when the event is set. The event is a `threading.Event` or an `asyncio.Event`. Without `atomic`, a stopped upload leaves the bytes sent so far at the destination. |
| `atomic`      | bool                     | Writes through a temporary file in the destination directory, then renames the file into place. The default is `False`. |

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

await sandbox.fs.uploadFile('large_dataset.csv', '/home/daytona/workspace/dataset.csv', {
  onProgress: (sent, total) => console.log(`Uploaded ${sent} of ${total} bytes`),
})
```

| **Parameter**        | **Type**                   | **Description**                                                                                              |
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `localPath`          | string                   | Sets the local file to upload. This parameter is required. |
| `remotePath`         | string                   | Sets the destination path in the sandbox. This parameter is required. |
| `options.onProgress` | TransferProgressCallback | Reports upload progress. The callback receives the bytes sent so far and the total file size in bytes. Errors thrown inside the callback are ignored. |
| `options.signal`     | AbortSignal              | Stops the upload when the signal is aborted. Without `options.atomic`, a stopped upload leaves the bytes sent so far at the destination. |
| `options.atomic`     | boolean                  | Writes through a temporary file in the destination directory, then renames the file into place. The default is `false`. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- `uploadFile` works in Node.js only, not in a browser.
</Callout>

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox cp large_dataset.csv {sandboxId}:/home/daytona/workspace/dataset.csv
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/upload?path=/home/daytona/workspace/dataset.csv' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --form 'file=@large_dataset.csv'
```

</TabItem>
</Tabs>

## Download files [#download-files]

### Download a single file [#download-a-single-file]

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/file1.txt", "Content of file 1")

content = sandbox.fs.read_bytes("/home/daytona/file1.txt")
print(content.decode("utf-8"))
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/file1.txt', 'Content of file 1')

const content = await sandbox.fs.readBytes('/home/daytona/file1.txt')
console.log(new TextDecoder().decode(content))
```

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox cp {sandboxId}:/home/daytona/file1.txt file1.txt
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- A local destination that ends in `/` or names a directory keeps the sandbox file name.
</Callout>

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/download?path=/home/daytona/file1.txt' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

#### Download a text file [#download-a-text-file]

Download a text file. The SDK downloads the file and decodes its content as UTF-8.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/file1.txt", "Content of file 1")

content = sandbox.fs.read_text("/home/daytona/file1.txt")
print(content)
```

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The file must be valid UTF-8.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/file1.txt', 'Content of file 1')

const content = await sandbox.fs.readText('/home/daytona/file1.txt')
console.log(content)
```

</TabItem>
</Tabs>

### Download multiple files [#download-multiple-files]

Download several files in one request. Each file is read independently, so a missing or unreadable file does not stop the others; it is reported as an error for its path. The SDK methods hold every downloaded file in memory before they return.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_many({
    "/home/daytona/data/file1.txt": b"Content of file 1",
    "/home/daytona/data/file2.txt": b"Content of file 2",
})

result = sandbox.fs.read_many(["/home/daytona/data/file1.txt", "/home/daytona/data/file2.txt"])

for path, content in result.files.items():
    print(f"Downloaded {path} ({len(content)} bytes)")

# Partial success is reported in the result, not raised
for error in result.errors:
    print(f"Error downloading {error.path}: {error.message}")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeMany({
  '/home/daytona/data/file1.txt': new TextEncoder().encode('Content of file 1'),
  '/home/daytona/data/file2.txt': new TextEncoder().encode('Content of file 2'),
})

const result = await sandbox.fs.readMany(['/home/daytona/data/file1.txt', '/home/daytona/data/file2.txt'])

Object.entries(result.files).forEach(([path, content]) => {
  console.log(`Downloaded ${path} (${content.byteLength} bytes)`)
})

// Partial success is reported in the result, not thrown
result.errors.forEach(error => console.error(`Error downloading ${error.path}: ${error.message}`))
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/bulk-download' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{
  "paths": [
    "/home/daytona/data/file1.txt",
    "/home/daytona/data/file2.txt"
  ]
}'
```

The response has the following fields:

| **Field** | **Description** |
| --------- | --------------- |
| `file` | One part per downloaded file. The filename of the part is the requested path. |
| `error` | One part per path that cannot be read, with a JSON body that holds `code`, `message`, `source`, and `path`. A path that cannot be read does not fail the request. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The response is `multipart/form-data` with the fixed boundary `DAYTONA-FILE-BOUNDARY`, not JSON.
</Callout>

</TabItem>
</Tabs>

### Download large files [#download-large-files]

Download large files to local disk. The SDK streams the file in chunks without holding it in memory, and reports progress as it receives. It writes to a temporary file next to the destination and renames it into place only after the whole file arrives, so a failed or cancelled download leaves an existing destination file unchanged. The CLI writes the file to disk as it arrives, over a [presigned URL](#presigned-urls) that it creates for the transfer.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/workspace/large-file.bin", "hello")

def on_progress(received: int, total: int | None) -> None:
    print(f"Downloaded {received} of {total} bytes")

sandbox.fs.download_file("/home/daytona/workspace/large-file.bin", "local_copy.bin", on_progress=on_progress)
```

| **Parameter** | **Type**                   | **Description**                                                                                                  |
| ------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `remote_path` | str                      | Sets the file to download from the sandbox. This parameter is required. |
| `local_path`  | str \| os.PathLike\[str\]  | Sets the local destination. A replaced destination file keeps its permission bits. This parameter is required. The parent directory of the destination must exist. |
| `on_progress` | TransferProgressCallback | Reports download progress. The callback receives the bytes received so far and the total size in bytes, or `None` when the total is unknown. |
| `cancel`      | TransferCancelSignal     | Stops the download when the event is set. The event is a `threading.Event` or an `asyncio.Event`. |

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/workspace/large-file.bin', 'hello')

await sandbox.fs.downloadFile('/home/daytona/workspace/large-file.bin', 'local_copy.bin', {
  onProgress: (received, total) => console.log(`Downloaded ${received} of ${total} bytes`),
})
```

| **Parameter**        | **Type**                   | **Description**                                                                                                         |
| -------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `remotePath`         | string                   | Sets the file to download from the sandbox. This parameter is required. |
| `localPath`          | string                   | Sets the local destination. The parent directory of the destination must exist. A replaced destination file keeps its permission bits. This parameter is required. |
| `options.onProgress` | TransferProgressCallback | Reports download progress. The callback receives the bytes received so far and the total size in bytes, or `undefined` when the total is unknown. |
| `options.signal`     | AbortSignal              | Stops the download when the signal is aborted. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- `downloadFile` works in Node.js only, not in a browser.
</Callout>

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
daytona sandbox cp {sandboxId}:/home/daytona/workspace/large-file.bin local_copy.bin
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/download?path=/home/daytona/workspace/large-file.bin' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --output local_copy.bin
```

</TabItem>
</Tabs>

## Delete files [#delete-files]

Delete a file or directory. A directory is deleted, with everything in it, only when the recursive option is set; otherwise it fails. A symbolic link is deleted as a link; its target is kept.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/workspace/file.txt", "hello")

sandbox.fs.remove("/home/daytona/workspace/file.txt")
```

| **Parameter** | **Type** | **Description**                                                                                                 |
| ------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| `recursive`   | bool   | Deletes a directory and its contents when `True`. The default is `False`. Without this option, a directory path raises `BadRequestError`. |

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/workspace/file.txt', 'hello')

await sandbox.fs.remove('/home/daytona/workspace/file.txt')
```

| **Parameter** | **Type**  | **Description**                                                                                             |
| ------------- | --------- | ----------------------------------------------------------------------------------------------------------- |
| `recursive`   | boolean | Deletes a directory and its contents when `true`. Without this option, a directory path throws `BadRequestError`. |

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files?path=/home/daytona/workspace/file.txt' \
  --request DELETE \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

| **Field**   | **Type** | **Description**                                                                                   |
| ----------- | -------- | ------------------------------------------------------------------------------------------------- |
| `recursive` | string | Deletes a directory and its contents when set to `true`. Without this field, a directory path returns `400`. |

</TabItem>
</Tabs>

## File permissions [#file-permissions]

Change the permission mode, owner, or group of a file or directory; at least one is required. The mode is an octal string. Owner and group accept a numeric ID or a name, which the sandbox resolves from its user and group lists. Symbolic links are followed.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/workspace/file.txt", "hello")

sandbox.fs.chmod("/home/daytona/workspace/file.txt", mode="644")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/workspace/file.txt', 'hello')

await sandbox.fs.chmod('/home/daytona/workspace/file.txt', { mode: '644' })
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/permissions?path=/home/daytona/workspace/file.txt&mode=644' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

## Search files by pattern [#search-files-by-pattern]

Search for files by name. The search walks the directory tree without following symbolic links and matches the name of every entry against the pattern, so directories with a matching name are returned as well. Directories the sandbox cannot read are skipped without an error, and a path that does not exist returns an empty result.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/workspace/main.py", "print('hello')")

# Recursive; the pattern is matched against file names with * and ? wildcards
result = sandbox.fs.glob("/home/daytona/workspace", "*.py")
for file in result.items:
    print(file)
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/workspace/main.py', "print('hello')")

// Recursive; the pattern is matched against file names with * and ? wildcards
const result = await sandbox.fs.glob('/home/daytona/workspace', '*.py')
result.items.forEach(file => console.log(file))
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/search?path=/home/daytona/workspace&pattern=*.py' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

Returned paths are prefixed with the path you passed. `skipped` lists up to 1000 paths below the root that the sandbox user cannot read, so their contents were not searched:

```json
{
  "files": [
    "/home/daytona/workspace/main.py",
    "/home/daytona/workspace/lib/util.py"
  ],
  "skipped": []
}
```

</TabItem>
</Tabs>

## Find and replace text in files [#find-and-replace-text-in-files]

Find text in files and replace it. Find checks each file line by line and returns the file, the line number starting at 1, and the full line for every match. Files that are not valid UTF-8 text are skipped, and a path that does not exist returns no matches. Replace takes a list of files, not directories, and rewrites each file in full with every occurrence replaced.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/workspace/src/app.py", "text-of-interest")
sandbox.fs.write_text("/home/daytona/workspace/file1.txt", "old_text")
sandbox.fs.write_text("/home/daytona/workspace/file2.txt", "old_text")

# Search for text in files; the pattern is a literal substring, not a regular expression.
# If a directory is given, the search is recursive.
results = sandbox.fs.grep("/home/daytona/workspace/src", "text-of-interest")
for match in results.items:
    print(f"{match.file}:{match.line}: {match.content}")

# Replace every occurrence of a literal substring in each listed file
result = sandbox.fs.replace(["/home/daytona/workspace/file1.txt", "/home/daytona/workspace/file2.txt"], "old_text", "new_text")

# Per-file failures are reported in the result, not raised
for item in result.items:
    if not item.success:
        print(f"Failed to update {item.file}: {item.error}")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/workspace/src/app.py', 'text-of-interest')
await sandbox.fs.writeText('/home/daytona/workspace/file1.txt', 'old_text')
await sandbox.fs.writeText('/home/daytona/workspace/file2.txt', 'old_text')

// Search for text in files; the pattern is a literal substring, not a regular expression.
// If a directory is given, the search is recursive.
const results = await sandbox.fs.grep('/home/daytona/workspace/src', 'text-of-interest')
results.items.forEach(match => console.log(`${match.file}:${match.line}: ${match.content}`))

// Replace every occurrence of a literal substring in each listed file
const result = await sandbox.fs.replace(['/home/daytona/workspace/file1.txt', '/home/daytona/workspace/file2.txt'], 'old_text', 'new_text')

// Per-file failures are reported in the result, not thrown
result.items
  .filter(item => !item.success)
  .forEach(item => console.error(`Failed to update ${item.file}: ${item.error}`))
```

</TabItem>
<TabItem label="API" icon="seti:json">

Find text in files:

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/find?path=/home/daytona/workspace/src&pattern=text-of-interest' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

`items` holds the matches in walk order. `skipped` lists up to 1000 paths below the root that the sandbox user cannot read, so they were not searched:

```json
{
  "items": [
    { "file": "/home/daytona/workspace/src/main.py", "line": 12, "content": "print('text-of-interest')" }
  ],
  "skipped": []
}
```

Replace text in files:

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/replace' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data '{
  "files": [
    "/home/daytona/workspace/file1.txt",
    "/home/daytona/workspace/file2.txt"
  ],
  "newValue": "new_text",
  "pattern": "old_text"
}'
```

```json
[
  { "file": "/home/daytona/workspace/file1.txt", "success": true },
  { "file": "/home/daytona/workspace/file2.txt", "success": false, "error": "No such file or directory (os error 2)" }
]
```

</TabItem>
</Tabs>

## Move or rename directory or file [#move-or-rename-directory-or-file]

Move or rename a file or directory. A move never replaces or merges into an existing destination entry. Across filesystems, the sandbox copies the entry, keeping each copied entry's owner and mode, and removes the source only after the whole copy succeeds.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.write_text("/home/daytona/workspace/data/file.txt", "hello")

# Missing parent directories are created; an existing destination is rejected with ConflictError
sandbox.fs.move("/home/daytona/workspace/data/file.txt", "/home/daytona/workspace/archive/file.txt")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.writeText('/home/daytona/workspace/data/file.txt', 'hello')

// Missing parent directories are created; an existing destination is rejected with ConflictError
await sandbox.fs.move('/home/daytona/workspace/data/file.txt', '/home/daytona/workspace/archive/file.txt')
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl '{toolboxProxyUrl}/{sandboxId}/files/move?source=/home/daytona/workspace/data/file.txt&destination=/home/daytona/workspace/archive/file.txt' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

</TabItem>
</Tabs>

## Watch for file changes [#watch-for-file-changes]

Watch a directory for changes. The watch streams create, write, remove, rename, and permission change events as they happen; it does not replay earlier changes. By default, only entries directly inside the directory are watched. A recursive watch also covers subdirectories, including ones created later. Event paths are relative to the watched directory.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.mkdir("/home/daytona/workspace/src")

handle = sandbox.fs.watch_dir(
    "/home/daytona/workspace/src",
    lambda event: print(f"{event.type}: {event.name}"),
)

sandbox.fs.write_text("/home/daytona/workspace/src/new.py", "print('hello')")

handle.stop()
```

| **Parameter** | **Type**                                | **Description**                                                                                           |
| ------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `on_event`    | Callable\[\[FilesystemEvent\], None \| Awaitable\[None\]\] | Receives each event. This is the second positional argument and is required. |
| `recursive`   | bool                                  | Watches subdirectories when `True`. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. The default is `False`. |
| `on_overflow` | Callable\[\[\], None \| Awaitable\[None\]\] | Runs when events are dropped. If events outpace delivery, the watch drops them and sends an overflow notice. |
| `on_exit`     | Callable\[\[Exception \| None\], None \| Awaitable\[None\]\] | Runs once when the watch ends, with `None` after `stop()` and otherwise with the error that ended the watch. Exceptions raised inside the callback are ignored. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The path must be absolute and name an existing directory.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.mkdir('/home/daytona/workspace/src')

const handle = await sandbox.fs.watchDir('/home/daytona/workspace/src', (event) => {
  console.log(`${event.type}: ${event.name}`)
})

await sandbox.fs.writeText('/home/daytona/workspace/src/new.py', "print('hello')")

await handle.stop()
```

| **Parameter** | **Type**     | **Description**                                                                   |
| ------------- | ------------ | --------------------------------------------------------------------------------- |
| `onEvent`     | (event: FilesystemEvent) => void \| Promise\<void> | Receives each event. An async callback is awaited before the next event. This is the second argument and is required. |
| `recursive`   | boolean    | Watches subdirectories when you pass `{ recursive: true }` as the third argument. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. |
| `onOverflow`  | () => void \| Promise\<void> | Runs when events are dropped. If events outpace delivery, the watch drops them and sends an overflow notice. Pass this callback in the options. |
| `onExit`      | (error?: Error) => void \| Promise\<void> | Runs once when the watch ends, with no error after `stop()` and otherwise with the error that ended the watch. Errors thrown inside the callback are ignored. Pass this callback in the options. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The path must be absolute and name an existing directory.
</Callout>

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl --no-buffer '{toolboxProxyUrl}/{sandboxId}/files/watch?path=/home/daytona/workspace/src' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

```text
event: start
data: {}

event: fs
data: {"type":"create","name":"new.py","isDir":false,"timestamp":1789121730123}

event: overflow
data: {"dropped":true}
```

| **Field**   | **Type**  | **Description**                         |
| ----------- | --------- | --------------------------------------- |
| `recursive` | boolean | Watches subdirectories when set to `true`. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. |

<Callout type="note" title="Notes">
- The request requires the `write:sandboxes` permission.
- The path must be absolute and name an existing directory.
</Callout>

</TabItem>
</Tabs>

### Wait for the watch to end [#wait-for-the-watch-to-end]

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
import threading

from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()
sandbox.fs.mkdir("/home/daytona/workspace/src")

handle = sandbox.fs.watch_dir(
    "/home/daytona/workspace/src",
    lambda event: print(f"{event.type}: {event.name}"),
)

# Nothing ends a healthy watch on its own: stop it from another thread,
# or the stream ends when the sandbox stops or is deleted
threading.Timer(60, handle.stop).start()

# Blocks until the watch is stopped or the stream ends
error = handle.wait_for_exit()
if error is not None:
    print(f"Watch ended: {error}")
```

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()
await sandbox.fs.mkdir('/home/daytona/workspace/src')

const handle = await sandbox.fs.watchDir('/home/daytona/workspace/src', (event) => {
  console.log(`${event.type}: ${event.name}`)
})

// Nothing ends a healthy watch on its own: stop it from elsewhere,
// or the stream ends when the sandbox stops or is deleted
setTimeout(() => handle.stop(), 60_000)

// Resolves when the watch is stopped or the stream ends
const error = await handle.waitForExit()
if (error) console.error('Watch ended:', error.message)
```

</TabItem>
</Tabs>

## Presigned URLs [#presigned-urls]

Create presigned URLs. A presigned URL downloads or uploads one sandbox file without an API key until it expires, so any HTTP client can transfer the file. Daytona signs each URL with the sandbox's signing key; rotating it invalidates all its outstanding URLs within 30 seconds.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()
sandbox = daytona.v2.create()

# Download link, valid for 1 hour (default)
download_url = sandbox.fs.presigned_download_url("/home/daytona/workspace/report.pdf")

# Upload link, valid for 1 hour (default)
upload_url = sandbox.fs.presigned_upload_url("/home/daytona/workspace/incoming/data.bin")

# Invalidate every outstanding presigned URL for this sandbox; old URLs can still work for up to 30 seconds
sandbox.rotate_signing_key()
```

| **Parameter**        | **Type** | **Description**                                                                                                            |
| -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `expires_in_seconds` | int    | Sets the URL lifetime in seconds, from `1` to `604800`. A URL is valid for at most `7` days. A value outside that range raises `BadRequestError` before any request is sent. The default is `3600`. |

<Callout type="note" title="Notes">
- The requests require the `write:sandboxes` permission.
- Windows sandboxes cannot have presigned URLs.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.v2.create()

// Download link, valid for 1 hour (default)
const downloadUrl = await sandbox.fs.presignedDownloadUrl('/home/daytona/workspace/report.pdf')

// Upload link, valid for 1 hour (default)
const uploadUrl = await sandbox.fs.presignedUploadUrl('/home/daytona/workspace/incoming/data.bin')

// Invalidate every outstanding presigned URL for this sandbox; old URLs can still work for up to 30 seconds
await sandbox.rotateSigningKey()
```

| **Parameter**      | **Type** | **Description**                                                                                                                      |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `expiresInSeconds` | number | Sets the URL lifetime in seconds, as an integer from `1` to `604800`. A URL is valid for at most `7` days. A value outside that range throws `BadRequestError` before any request is sent. The default is `3600`. |

<Callout type="note" title="Notes">
- The requests require the `write:sandboxes` permission.
- Windows sandboxes cannot have presigned URLs.
</Callout>

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
# Expiry 1 hour from now (GNU date; on macOS use: date -u -v+1H +%Y-%m-%dT%H:%M:%SZ)
EXPIRES_AT=$(date -u -d '+1 hour' +%Y-%m-%dT%H:%M:%SZ)

# Sign a download link: {"url": "...", "method": "GET", "path": "...", "expires_at": "...", "signature": "v1_..."}
# Use "method": "POST" to sign an upload link
curl 'https://api.daytona.com/sandboxes/{sandboxId}/files/sign' \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --data "{ \"method\": \"GET\", \"path\": \"/home/daytona/workspace/report.pdf\", \"expires_at\": \"$EXPIRES_AT\" }"

# Consume a download link: use the returned url as is (no Authorization header)
curl --output report.pdf \
  '{toolboxProxyUrl}/{sandboxId}/files/download?path=%2Fhome%2Fdaytona%2Fworkspace%2Freport.pdf&expires={expires}&signature={signature}'

# Consume an upload link
curl --request POST \
  --form 'file=@data.bin' \
  '{toolboxProxyUrl}/{sandboxId}/files/upload?path=%2Fhome%2Fdaytona%2Fworkspace%2Fincoming%2Fdata.bin&expires={expires}&signature={signature}'

# Rotate the signing key to revoke all outstanding links; old links can still work for up to 30 seconds
curl 'https://api.daytona.com/sandboxes/{sandboxId}/signing-key/rotate' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

| **Field**  | **Type**  | **Description**                                                                                          |
| ---------- | --------- | -------------------------------------------------------------------------------------------------------- |
| `expires_at` | string | Sets the expiry as an RFC 3339 timestamp. The expiry must lie in the future and at most `7` days ahead. This field is required. |
| `method`   | string  | Sets the link type: `GET` for a download link and `POST` for an upload link, in any letter case. Any other method returns `400`. This field is required. |
| `path`     | string  | Sets the absolute sandbox path of the file to download or upload. The `path` query parameter of the returned URL must match it exactly. This field is required. |

<Callout type="note" title="Notes">
- The requests require the `write:sandboxes` permission.
- Windows sandboxes cannot have presigned URLs.
</Callout>

</TabItem>
</Tabs>

## Permissions [#permissions]

File system access runs through the toolbox of a sandbox. Access is governed by one project permission, which you can grant to an [organization role](/docs/organizations#roles) or an [API key](/docs/api-keys).

A [sandbox access token](/docs/sandboxes#create-an-access-token) with the toolbox:fs operation grants the same access to a single sandbox, without an API key. The sandbox read permission alone does not grant toolbox access.

| **Permission**        | **Grants**                                           |
| --------------------- | ---------------------------------------------------- |
| **`write:sandboxes`** | List, read, upload, download, move, and delete files |
