# How to mount an S3 bucket in a sandbox Store the bucket's key as a Runtime secret. Then call `sbx.mounts.add({ provider: "s3", bucket, path, secret })`. **On Runtime, the bucket's key stays outside the sandbox.** You store the key once, as a secret. The mount client inside the sandbox signs its requests with a placeholder. Runtime's egress proxy signs each request again with the real key, as it leaves for the bucket's endpoint. So code in the sandbox, or a prompt injection that steers it, has nothing worth stealing. Amazon S3, Cloudflare R2, Google Cloud Storage and any S3-compatible store work. A public bucket needs no key at all ([mount your own bucket](/docs/storage#mount-your-own-bucket)). ## Store the key and mount the bucket ```bash no-run printf %s "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" | runtime secrets set DATA_BUCKET \ --host s3.eu-west-1.amazonaws.com --header Authorization --format 'AWS4-HMAC-SHA256 {value}' runtime sandboxes mount "${id}" s3://my-data/datasets /data --secret DATA_BUCKET --region eu-west-1 runtime sandboxes mounts "${id}" runtime sandboxes unmount "${id}" /data ``` ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.secrets.set("DATA_BUCKET", { value: `${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}`, hosts: ["s3.eu-west-1.amazonaws.com"], header: "Authorization", format: "AWS4-HMAC-SHA256 {value}", }); await using sbx = await runtime.sandboxes.create(); await sbx.mounts.add({ provider: "s3", bucket: "my-data", prefix: "datasets", region: "eu-west-1", path: "/data", secret: "DATA_BUCKET", }); console.log((await sbx.exec("ls -la /data")).stdout); await sbx.exec("echo 'a,b' > /data/result.csv", { check: true }); // reaches the bucket on close ``` ```python check import os from withruntime import Runtime runtime = Runtime() runtime.secrets.set( "DATA_BUCKET", value=f"{os.environ['AWS_ACCESS_KEY_ID']}:{os.environ['AWS_SECRET_ACCESS_KEY']}", hosts=["s3.eu-west-1.amazonaws.com"], header="Authorization", format="AWS4-HMAC-SHA256 {value}", ) with runtime.sandboxes.create() as sbx: sbx.mounts.add(provider="s3", bucket="my-data", prefix="datasets", region="eu-west-1", path="/data", secret="DATA_BUCKET") print(sbx.exec("ls -la /data").stdout) ``` The secret must match exactly. Its value is `ACCESS_KEY_ID:SECRET_ACCESS_KEY`, with `:SESSION_TOKEN` added for temporary keys. Its header is `Authorization`. Its format is `AWS4-HMAC-SHA256 {value}`. Its host is the bucket's endpoint. ## R2, Google Cloud Storage and other stores | Provider | Mount with | The secret's host | | -------------------- | --------------------------------------------------- | --------------------------------------- | | Amazon S3 | `provider: "s3"`, `region` (us-east-1 when omitted) | `s3..amazonaws.com` | | Cloudflare R2 | `provider: "r2"`, `accountId` | `.r2.cloudflarestorage.com` | | Google Cloud Storage | `provider: "gcs"`, with an HMAC key | `storage.googleapis.com` | | Any S3-compatible | `provider: "s3"`, `endpoint` | Your store's own host | | A public bucket | Any of the above, no `secret` | None | ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); await sbx.mounts.add({ provider: "r2", accountId: "", bucket: "models", path: "/models", secret: "R2_MODELS", readOnly: true, }); console.log(await sbx.mounts.list()); ``` `prefix` mounts only the keys under it. `readOnly` refuses writes. That suits a shared dataset that jobs must not change. ## How the mount behaves | Question | Answer | | ------------------------------- | -------------------------------------------------------------------------------- | | What runs it | rclone, installed in the sandbox the first time you mount. It adds a few seconds | | When a write reaches the bucket | When the file is closed | | Two sandboxes, one bucket | Both can mount it. A file that both write ends with the version closed last | | Pause and wake | The mount lasts through both | | Stop and restart | The mount ends at a stop. Mount it again after a restart | | Network | HTTPS to the endpoint, through the sandbox's own rules, like any other host | | Where the key lives | Sealed on Runtime's servers. No API, tool or command returns it | ## A mount, a volume or the AWS SDK? - **A mount** fits data that already lives in a bucket: datasets, model files, results other systems read. - **A [volume](/how-to/back-up-a-volume)** is a disk that outlives sandboxes. It sits on the sandbox's own server. Use it for data a program writes and rereads many times, such as a package cache or a database file. - **The AWS SDK with an identity token** fits code that calls S3's API itself, for listing, presigned URLs or other AWS services. It needs no key at all ([use identity tokens with AWS](/how-to/use-identity-tokens-with-aws)). ## Mistakes and how Runtime handles them - **The secret's host does not match the endpoint.** The proxy adds the real key only on HTTPS requests to the secret's hosts. For S3, that is the regional host, `s3..amazonaws.com`. - **Pasting the key into an environment variable.** Then the key lives in the sandbox. With the secret, a sandbox that prints its whole environment shows only a worthless placeholder. - **Reading a file from elsewhere right after writing it.** A write reaches the bucket when the file is closed. Close the file, or finish the program, first. - **An allow list that leaves the bucket out.** The bucket is reached through the sandbox's network rules. Allow its endpoint host when you narrow them ([turn off sandbox internet](/how-to/turn-off-sandbox-internet)). - **An MCP agent.** The same mounts are the `runtime_sandboxes_mounts` tool's `mount`, `list` and `unmount` actions. ## Start ```bash no-run npx withruntime sandboxes run --keep -- ls /workspace ``` A new account gets 100 free hours of a 1 GB machine in its first 30 days, with no card needed. Then mount a bucket into the sandbox it kept, with `runtime sandboxes mount`. Facts on this page were checked on 26 October 2026.