Skip to main content
Preview: the mounts API is new. Endpoints, request shape, and SDK method names may change before GA. Backend coverage is rclone’s — works for the listed providers today; expect rough edges on less-tested backends.
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.
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 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 for that backend type.
BackendCommon keys
s3access_key_id, secret_access_key, region, provider (default AWS), endpoint
gcsservice_account_credentials (JSON string) or service_account_file
azureblobaccount, key or sas_url
sftphost, user, pass or key_file, port
webdavurl, vendor, user, pass
dropboxtoken
For anything not in this list, or for advanced tuning, pass rcloneConfig directly with a raw rclone config string — see Custom Config below.

Examples

S3 (AWS)

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",
  },
});

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

Override the provider and point at the endpoint:
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

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"),
  },
});

SFTP

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.
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:
await sandbox.mounts.add({
  path: "/mnt/writable",
  remote: "s3:scratch",
  backend: "s3",
  creds: {...},
  readOnly: false,    // opts into RW; uses --vfs-cache-mode writes
});
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.

Listing and removing

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.
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:
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

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:
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 a new image off the current Dockerfile.default. Mount succeeds but ls returns empty — usually a creds / permission issue on the remote side rather than a mount issue. SSH in (oc shell) and run rclone ls <remote>: --config /run/oc-agent/mounts/<id>.conf to see the real error. Or just ls -la the mount point — FUSE errors surface as filesystem errors there. 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.
CLI equivalent: oc mounts. Full reference: TypeScript SDK · Python SDK.