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

# Mounts

> Mount S3, GCS, Azure Blob, SFTP, WebDAV, and Dropbox into a sandbox via FUSE

<Note>
  **Preview:** the mounts API is new. Endpoints, request shape, and SDK
  method names may change before GA.
</Note>

Mount remote filesystems directly into your sandbox so your code reads and
writes them like local files — no SDK calls, no chunked downloads, no
boilerplate. Perfect for pulling in datasets, model weights, or a customer's
bucket without staging anything to disk first.

```typescript theme={null}
import { Sandbox } from "@opencomputer/sdk";

const sandbox = await Sandbox.create();

await sandbox.mounts.add({
  path: "/mnt/data",
  remote: "s3:my-bucket/datasets",
  backend: "s3",
  creds: {
    access_key_id: process.env.AWS_ACCESS_KEY_ID!,
    secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!,
    region: "us-east-1",
  },
});

// Now anything in the sandbox can read s3://my-bucket/datasets as /mnt/data:
await sandbox.exec.run("ls /mnt/data");
await sandbox.exec.run("head /mnt/data/train.csv");
```

## How it works

Mounts use [`rclone mount`](https://rclone.org/commands/rclone_mount/) under the
hood — a single binary that speaks \~40 backends (S3, GCS, Azure Blob, SFTP,
WebDAV, Dropbox, and more). When you call `mounts.add()`:

1. The control plane templates an rclone config from your `backend` + `creds`.
2. The config is written to a tmpfs file inside the VM (mode `0600` — the
   sandbox user can't read it).
3. `rclone mount` is spawned as a background daemon, exposing the remote at
   the path you specified.
4. Every process in the sandbox can now read/write that path.

Credentials live only inside the VM's tmpfs for the lifetime of the mount.
The worker keeps no copy.

## Supported Backends

`backend` selects how `creds` are templated. The keys you pass map directly to
[rclone config fields](https://rclone.org/docs/) for that backend type.

| Backend     | Common keys                                                                            |
| ----------- | -------------------------------------------------------------------------------------- |
| `s3`        | `access_key_id`, `secret_access_key`, `region`, `provider` (default `AWS`), `endpoint` |
| `gcs`       | `service_account_credentials` (JSON string) or `service_account_file`                  |
| `azureblob` | `account`, `key` *or* `sas_url`                                                        |
| `sftp`      | `host`, `user`, `pass` or `key_file`, `port`                                           |
| `webdav`    | `url`, `vendor`, `user`, `pass`                                                        |
| `dropbox`   | `token`                                                                                |

For anything not in this list, or for advanced tuning, pass `rcloneConfig`
directly with a raw rclone config string — see [Custom Config](#custom-config)
below.

## Examples

### S3 (AWS)

<CodeGroup>
  ```typescript TypeScript theme={null}
  await sandbox.mounts.add({
    path: "/mnt/data",
    remote: "s3:my-bucket",
    backend: "s3",
    creds: {
      access_key_id: process.env.AWS_ACCESS_KEY_ID!,
      secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!,
      region: "us-east-1",
    },
  });
  ```

  ```python Python theme={null}
  await sandbox.mounts.add(
      path="/mnt/data",
      remote="s3:my-bucket",
      backend="s3",
      creds={
          "access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
          "secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"],
          "region": "us-east-1",
      },
  )
  ```
</CodeGroup>

### S3-compatible (MinIO, R2, Tigris, etc.)

Override the `provider` and point at the endpoint:

```typescript theme={null}
await sandbox.mounts.add({
  path: "/mnt/r2",
  remote: "r2:my-bucket",
  backend: "s3",
  creds: {
    provider: "Cloudflare",
    access_key_id: "...",
    secret_access_key: "...",
    endpoint: "https://<accountid>.r2.cloudflarestorage.com",
    region: "auto",
  },
});
```

### Google Cloud Storage

<CodeGroup>
  ```typescript TypeScript theme={null}
  import fs from "node:fs";

  await sandbox.mounts.add({
    path: "/mnt/gcs",
    remote: "gcs:my-bucket",
    backend: "gcs",
    creds: {
      service_account_credentials: fs.readFileSync("./sa-key.json", "utf8"),
    },
  });
  ```

  ```python Python theme={null}
  with open("./sa-key.json") as f:
      sa_json = f.read()

  await sandbox.mounts.add(
      path="/mnt/gcs",
      remote="gcs:my-bucket",
      backend="gcs",
      creds={"service_account_credentials": sa_json},
  )
  ```
</CodeGroup>

### SFTP

```typescript theme={null}
await sandbox.mounts.add({
  path: "/mnt/remote",
  remote: "ssh:/home/data",
  backend: "sftp",
  creds: {
    host: "data.example.com",
    user: "agent",
    pass: process.env.SSH_PASSWORD!, // or key_file
  },
});
```

### Custom Config

For backends not in the typed list — or to tune things like `--vfs-cache-mode`
or `--dir-cache-time` — pass an rclone config string directly. The section
name in the config must match the part of `remote` before the colon.

```typescript theme={null}
await sandbox.mounts.add({
  path: "/mnt/box",
  remote: "box:Reports",
  rcloneConfig: `
[box]
type = box
token = {"access_token":"...","refresh_token":"...","expiry":"..."}
`.trim(),
  mountOptions: ["--dir-cache-time", "1m"],
});
```

## Read-only by default

Mounts are read-only unless you explicitly opt into read-write:

```typescript theme={null}
await sandbox.mounts.add({
  path: "/mnt/writable",
  remote: "s3:scratch",
  backend: "s3",
  creds: {...},
  readOnly: false,    // opts into RW; uses --vfs-cache-mode writes
});
```

<Warning>
  Object-store FUSE mounts have well-known write footguns — concurrent writers
  to the same key can produce surprising results, and small-write workloads
  amplify request counts (and your bill). Prefer object-store SDKs for
  write-heavy workloads; use mounts for read-heavy access and append-style
  artifacts.
</Warning>

## Listing and removing

```typescript theme={null}
const mounts = await sandbox.mounts.list();
// [{ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", readOnly: true }]

await sandbox.mounts.remove("/mnt/data");
```

## Hibernate behavior

Mounts survive hibernate/wake transparently. The VM snapshot captures the live
FUSE mount and the rclone daemon process; when the sandbox wakes, both are
restored as-is. No re-mount call needed — the mount is exactly where you left
it, with the same credentials, serving the same path.

```typescript theme={null}
await sandbox.mounts.add({ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", creds });

await sandbox.hibernate();
await sandbox.wake();

await sandbox.exec.run("ls /mnt/data");   // still works, no re-mount needed
```

When you want a mount gone, call `remove()` explicitly:

```typescript theme={null}
await sandbox.mounts.remove("/mnt/data");
```

That triggers an actual `fusermount3 -u` in the VM and drops the registry
entry. No magic teardown on hibernate.

### Stale credentials

If the credentials behind a mount get rotated or revoked while the sandbox is
hibernated, the in-VM rclone daemon — restored intact from the snapshot — is
still holding the *old* keys and will start hitting auth errors on the next
request. Fix: `mounts.remove(path)` then `mounts.add(...)` again with fresh
credentials.

## CLI

```bash theme={null}
oc mounts add sb-abc123 \
  --path /mnt/data \
  --remote s3:my-bucket \
  --backend s3 \
  --cred access_key_id=AKIA... \
  --cred secret_access_key=... \
  --cred region=us-east-1

oc mounts list sb-abc123
oc mounts rm sb-abc123 /mnt/data
```

For raw rclone configs, point `--config-file` at a local file:

```bash theme={null}
oc mounts add sb-abc123 --path /mnt/box --remote box:Reports --config-file ./rclone.conf
```

## Troubleshooting

**`sandbox image is missing rclone and/or fusermount3`** — the sandbox is
running on an older base image. Recreate the sandbox from the latest default
template, or build an image off the current `Dockerfile.default`.

**Mount succeeds but `ls` returns empty** — usually a creds /
permission issue on the remote side. SSH in (`oc shell`) and run
`rclone ls <remote>: --config /run/oc-agent/mounts/<id>.conf` to see the real
error. Or `ls -la` the mount point — FUSE errors surface as filesystem errors.

**`fuse: bad mount point ... permission denied`** — make sure the target path
isn't already a non-empty directory you don't own. The mount call creates the
path with `sandbox:sandbox` ownership; pre-existing roots can conflict.

<Tip>
  CLI equivalent: [`oc mounts`](/cli/mounts). Full reference:
  [TypeScript SDK](/reference/typescript-sdk#mounts) ·
  [Python SDK](/reference/python-sdk#mounts).
</Tip>

## Bring your own FUSE

<Note>
  Advanced — most users want the rclone path above. Reach for this only if you
  already have a FUSE-ready filesystem.
</Note>

Under the hood, `mounts.add()` picks a **driver** — the daemon that actually
establishes the FUSE mount. Everything above uses the default `rclone` driver.
If you already have a FUSE-ready filesystem (your own VFS, gcsfuse, s3fs, …) and
don't want rclone as a middle layer, use the **`command` driver**: you hand us
the daemon's argv and we run it as the sandbox user, inject your env/secrets,
wait for the mount to come live, and tear it down on `remove` — the same
lifecycle as an rclone mount.

```typescript theme={null}
await sandbox.mounts.add({
  path: "/mnt/data",
  driver: "command",
  command: ["my-vfs-fuse", "--bucket", "gs://my-bucket", "{mountpoint}"],
  secrets: { MY_VFS_TOKEN: process.env.MY_VFS_TOKEN! },
});
```

In Python this is a separate method, `mounts.add_command(...)`.

### The `{mountpoint}` token

`{mountpoint}` (or `{path}`) in your argv is replaced with `path` before the
command runs, so you don't repeat the path twice. The call above runs:

```
my-vfs-fuse --bucket gs://my-bucket /mnt/data
```

You can also hardcode the path — the token is just a convenience.

### env and secrets

* **`env`** — plain environment variables for the daemon. Recorded and returned
  by `list()`.
* **`secrets`** — credentials. Injected into the daemon's **process
  environment** (never the command line, so they don't leak via `ps`), never
  written to disk on the worker, and never returned by `list()`.

For daemons that need a credentials *file* (e.g. a service-account JSON), write
it first with the [filesystem API](/sandboxes/filesystem) and point the command
at it.

`readOnly` is **advisory** for this driver — your command must honor it (we also
export `OC_MOUNT_READONLY=1`). Unlike rclone, the platform can't enforce
read-only on an arbitrary daemon.

### Requirements

Your daemon must:

* be present in the sandbox — bake it into your image, or install/download it at
  runtime before mounting;
* mount at the mountpoint and keep running (a foreground daemon is fine — we
  background it; a self-daemonizing one works too);
* exit when its mount is unmounted (standard libfuse behavior) so `remove`
  cleans up.

The platform handles the rest: `/dev/fuse` access, creating the mountpoint, and
`-o allow_other` support (`user_allow_other` is set in `/etc/fuse.conf`). If the
mount doesn't come up within the timeout, the call fails with the **daemon's own
log tail** so you can see why (a bad flag, missing binary, auth error, etc.).

### Example: gcsfuse

[gcsfuse](https://cloud.google.com/storage/docs/gcsfuse-install) is Google's own
GCS-to-FUSE adapter. Install it in your image (or at runtime), write your
service-account key with the [filesystem API](/sandboxes/filesystem), and mount:

```typescript theme={null}
await sandbox.files.write("/run/gcp-sa.json", process.env.GCP_SA_JSON!);

await sandbox.mounts.add({
  path: "/mnt/bucket",
  driver: "command",
  command: [
    "gcsfuse", "--foreground", "--implicit-dirs",
    "-o", "allow_other",
    "--key-file", "/run/gcp-sa.json",
    "my-bucket", "{mountpoint}",
  ],
});

await sandbox.exec.run("ls /mnt/bucket");   // GCS objects, read like local files
```

For a **public** bucket, skip the key and pass `--anonymous-access`:

```typescript theme={null}
command: ["gcsfuse", "--foreground", "--anonymous-access", "-o", "allow_other",
          "gcp-public-data-landsat", "{mountpoint}"],
```

### Example: bindfs

A minimal local FUSE — mirror one directory onto another path:

```typescript theme={null}
await sandbox.exec.run("apt-get update && apt-get install -y bindfs");
await sandbox.mounts.add({
  path: "/mnt/mirror",
  driver: "command",
  command: ["bindfs", "-f", "-o", "allow_other", "/home/sandbox/data", "{mountpoint}"],
  readOnly: false,
});
// reads/writes under /mnt/mirror now pass through to /home/sandbox/data
```

### CLI

Pass `--command` once per argv element; `--env`/`--secret` take `key=value`:

```bash theme={null}
oc mounts add sb-abc123 \
  --path /mnt/mirror \
  --command bindfs --command -f --command -o --command allow_other \
  --command /home/sandbox/data --command '{mountpoint}' \
  --secret MY_VFS_TOKEN=...
```

If the mount doesn't come up, the error includes the **daemon's log tail** —
usually a missing binary, a bad flag, or an auth failure. Reproduce by hand with
`oc shell` and run the same command.
