Local Files
Mount storage into a sandbox with SandboxConfig.fs. Each entry defines a backend, a mount path inside the sandbox, and an acls list that controls access (see File Access). You can combine multiple backends in one sandbox, mount paths must be unique and non-overlapping.
| Backend | Persists beyond the sandbox? | Use it for |
|---|---|---|
local | No (unless snapshotted) | Ephemeral scratch space. The default. |
gdrive | Yes | Files that should land in a user's Google Drive. |
gcs | Yes | Durable object storage / shared datasets. |
s3 | Yes | Amazon S3 or an S3-compatible object store. |
azure | Yes | Azure Blob Storage. |
onedrive | Yes | Files that should land in a user's Microsoft OneDrive. |
external | Yes | A file system you back with your own HTTP host. |
This page covers the local and external backends. For the cloud backends see GCS, S3, Azure Blob, Google Drive, and OneDrive.
Local
Stores data on the sandbox's own disk. No external dependencies; discarded when the sandbox is destroyed (unless captured with a Snapshot). This is the default when you omit fs.
import { getOrCreateSandbox } from "@hiver.sh/client";
const sandbox = await getOrCreateSandbox("my-sandbox", {
fs: [{
backend: "local",
mount: "/workspace",
acls: [{ path: "/workspace/**", access: "rw" }],
}],
});Mount a host directory (Docker runtime only)
During local development, set origin to mount a directory from your machine straight into the sandbox, changes are visible both ways with no copying. This is only supported with the local Docker runtime and is ignored in cloud deployments.
fs: [{
backend: "local",
mount: "/workspace",
origin: "/Users/me/projects/my-project", // host path mounted into the sandbox
acls: [{ path: "/workspace/**", access: "rw" }],
}]Hot reload
Because the mount is a live view of your host directory, a file watcher inside the sandbox picks up edits you make in your editor instantly, no rebuild, no re-upload. Mount your project and run the app under a reloader:
const sandbox = await getOrCreateSandbox("dev", {
image: "python",
fs: [{
backend: "local",
mount: "/workspace",
origin: process.cwd(), // your project, live-mounted
acls: [{ path: "/workspace/**", access: "rw" }],
}],
});
// uvicorn --reload watches /workspace and restarts on every save you make locally
const proc = await sandbox.execStream(
["uvicorn", "app:main", "--host", "0.0.0.0", "--port", "8000", "--reload"],
{ cwd: "/workspace" },
);
for await (const chunk of proc.pipes) process.stdout.write(chunk.stdout ?? "");
// edit app.py in your editor → uvicorn reloads inside the sandboxThe same works for any watcher, nodemon, watchexec, flask --debug, a test runner in --watch mode. Reach the running server through the proxy: sandbox.proxyUrl(8000).
Skill iteration
Agents often load a directory of "skills" (prompts, scripts, tool definitions) at runtime. Mount that directory with origin so you can tweak a skill on your host and have the agent pick up the new version on its next run, no image rebuild between iterations:
const sandbox = await getOrCreateSandbox("agent-dev", {
image: "claude",
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
fs: [
{ backend: "local", mount: "/workspace", acls: [{ path: "/workspace/**", access: "rw" }] },
{
backend: "local",
mount: "/home/agent/.claude/skills",
origin: "./skills", // edit skills locally, agent sees them live
acls: [{ path: "/home/agent/.claude/skills/**", access: "ro" }],
},
],
});
// tweak ./skills/*.md on your host, then re-run, no rebuild needed
await sandbox.exec(["claude", "-p", "Use the pdf-report skill to summarize report.pdf"], {
cwd: "/workspace",
});Mounting the skills directory ro means the agent can read and use the skills but can't modify them, you stay in control of the source on your host.
Multiple mounts
Combine backends by listing several entries. Each mount is independent, writing to one never touches another.
fs: [
{ backend: "local", mount: "/workspace", acls: [{ path: "/workspace/**", access: "rw" }] },
{
backend: "gcs",
mount: "/data",
gcs_bucket: "my-data",
gcs_service_account_json: process.env.GCS_SERVICE_ACCOUNT_JSON!,
acls: [{ path: "/data/**", access: "ro" }], // read-only dataset from GCS
},
]External
Back a mount with your own storage by implementing a small HTTP host. Set backend: "external" and point host at your server, each file operation the agent performs (read, write, list, delete) becomes one call against it. Use this for a database, an internal object store, or any system without a built-in backend.
fs: [{
backend: "external",
mount: "/data",
host: "https://my-fs-host.internal", // trailing slash ignored
acls: [{ path: "/data/**", access: "rw" }],
}]An external host that serves fixtures from memory is the easy way to hand an agent canned files for tests and evals, see Mocking.
Hiding a mount from the agent
Set internal: true on any file system to mount it for the sandbox runtime but hide it from the agent. ACLs are ignored (the agent can't reach it at all). This is used for storage the sandbox needs but the agent must not see, for example, a remote-backed snapshot store.
Lazy, cached mounts (async)
By default a cloud-backed mount is read-through: the agent always sees the latest backend state, and every content read is a fresh synchronous round-trip — the same file re-downloads on every open, so the agent never observes a stale copy.
Set async: true to run the mount in fetch-on-read, cache-after mode:
- Nothing is fetched up front. The mount comes up instantly; startup issues no
GET. (There is no eager background pull — the mount does not walk the backend downloading objects.) - Metadata is synchronous, on demand.
statand directory listings hit the backend when asked (results are briefly cached), so the agent sees what exists without downloading anything. - Content is fetched lazily and synchronously, on the first explicit read. Opening a file for reading pays exactly one
GET; the bytes land in the local buffer and every later read of that file is served locally — no repeatGET. - Writes are buffered and uploaded in the background (the buffer is kept, so a file you wrote reads back from the buffer without a fetch).
{
backend: "gcs",
mount: "/workspace",
async: true,
gcs_bucket: "my-bucket",
}The contract in one line: a GET (content download) happens only in response to an explicit read, never before.
Worked example — what hits the backend, when
Given a backend holding /workspace/report.md and /workspace/data/big.csv, on a fresh async mount:
mount → (nothing — no GET)
ls /workspace → LIST (metadata only)
stat /workspace/report.md → STAT (metadata only; no content)
cat /workspace/report.md → GET report.md (first read: one content fetch → cached)
cat /workspace/report.md → (served from local buffer — no GET)
echo hi > /workspace/note → (buffered locally; PUT note uploads in the background)
cat /workspace/note → (served from local buffer — no GET)big.csv is never downloaded unless something actually reads it.
Symlinks
A symlink has no native object type in a blob store, so it is persisted as a companion object at <path>.symlink whose body is the link target. This round-trips through async without ever downloading a linked file eagerly:
ln -s /library/x.md out.md → PUT out.md.symlink (body = "/library/x.md")
ls /workspace/output → LIST → out.md shown as a symlink (no GET)
stat out.md → STAT out.md.symlink (metadata only; reported as a link)
readlink out.md → GET out.md.symlink (explicit read of the link → target)
cat out.md → GET x.md (reads the *target*, resolved lazily)Trade-off
Because content is cached after the first read, an async mount is eventually consistent for content: an out-of-band change to a file already in the local buffer isn't re-fetched. Use async when the workspace is effectively owned by one sandbox at a time (the common agent case) and you want backend durability without paying download latency up front or on every repeated read. Leave it off (the default read-through) when the agent must observe out-of-band backend content changes live. It has no effect on local backends, which have no backend to sync with.
Next: Google Cloud Storage