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

# Mount Filesystems

> Mount durable, shareable Tensorlake filesystems into sandboxes at boot, on warm-pool claims, or on a running sandbox, optionally read-only, prefetched, pinned to a permanent snapshot, or owned by a guest user of your choice.

A sandbox can mount [Tensorlake filesystems](/filesystems/introduction) as ordinary directories. The sandbox's root disk is ephemeral; a mounted filesystem is durable, survives the sandbox, and can be shared by many sandboxes at once. Reads stream in lazily, so mounting is fast regardless of how much the filesystem holds, and writes replicate to durable storage automatically through [autosave](/filesystems/concurrent-writes).

You mount a filesystem by the name you created it with:

```bash theme={null}
tl fs create data
```

That name (`data`) is the `file_system_id` everywhere in the sandbox API.

## Mount at Creation

Pass one or more mounts when creating the sandbox. The mounts are ready before the sandbox is reported as running.

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    tl sbx create -f data:/mnt/data
    ```

    `-f`/`--filesystem` takes `<name>[@<snapshot-id>]:<mount-path>[:<opts>]` and can be repeated. `<opts>` is a comma-separated list of `ro` (read-only) and/or `prefetch`; `@<snapshot-id>` pins the mount to a permanent snapshot and requires `ro` — see [Pinned mounts](#pinned-mounts):

    ```bash theme={null}
    tl sbx create -f data:/mnt/data:ro,prefetch -f scratch:/work
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from tensorlake.sandbox import FileSystemMount, Sandbox

    with Sandbox.create(
        image="tensorlake/ubuntu-minimal",
        file_systems=[
            FileSystemMount(file_system_id="data", mount_path="/mnt/data"),
        ],
    ) as sandbox:
        result = sandbox.run("ls", ["/mnt/data"])
        print(result.stdout)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Sandbox } from "tensorlake";

    const sandbox = await Sandbox.create({
      image: "tensorlake/ubuntu-minimal",
      fileSystems: [{ fileSystemId: "data", mountPath: "/mnt/data" }],
    });

    const result = await sandbox.run("ls", { args: ["/mnt/data"] });
    console.log(result.stdout);

    await sandbox.terminate();
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    curl -X POST https://api.tensorlake.ai/sandboxes \
      -H "Authorization: Bearer $TL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "file_systems": [
          {"file_system_id": "data", "mount_path": "/mnt/data"}
        ]
      }'
    ```

    Each mount accepts optional `read_only` and `prefetch` booleans (omitting them means `false`), an optional `snapshot_id` pin, and an optional `owner` user spec.
  </Tab>
</Tabs>

<Note>
  Create the filesystem first (`tl fs create <name>`). Mounting a name that does not exist fails the sandbox — see [Errors](#errors).
</Note>

### Mount rules

| Rule       | Detail                                                                                                                           |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Name       | `file_system_id` is the filesystem's name (ASCII letters, digits, `_`, `-`)                                                      |
| Path       | `mount_path` must be absolute and not `/`; paths are normalized (`//`, `.`, and trailing slashes collapse), and `..` is rejected |
| Uniqueness | No two mounts may share a path or nest under one another (`/mnt` and `/mnt/data` conflict)                                       |
| Count      | At most 8 filesystem mounts per sandbox                                                                                          |

## Mount Options

All options are per mount and default to off.

### Read-only

`read_only` mounts the filesystem read-only. Writes inside the guest fail with `EROFS`. Enforcement is defense-in-depth: the storage credential minted for the mount carries no write scope, the host proxy filters writes, and the guest mount itself is read-only.

Read-only is fail-closed: the sandbox is only placed on executor fleets that can enforce it, so a `read_only` mount can never silently degrade to read-write. On fleets that have not yet been updated, a sandbox requesting a read-only mount will not be placed.

### Prefetch

`prefetch` downloads the filesystem's full tree in the background after the mount is ready. The mount is usable immediately — reads stream in lazily in the meantime — and once the prefetch completes, reads no longer touch the network. Prefetch is best-effort: it never blocks mount readiness and never fails the sandbox. On older fleets it is skipped silently.

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    tl sbx create -f models:/mnt/models:ro,prefetch
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from tensorlake.sandbox import FileSystemMount, Sandbox

    sandbox = Sandbox.create(
        file_systems=[
            FileSystemMount(
                file_system_id="models",
                mount_path="/mnt/models",
                read_only=True,
                prefetch=True,
            ),
        ],
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const sandbox = await Sandbox.create({
      fileSystems: [
        {
          fileSystemId: "models",
          mountPath: "/mnt/models",
          readOnly: true,
          prefetch: true,
        },
      ],
    });
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    curl -X POST https://api.tensorlake.ai/sandboxes \
      -H "Authorization: Bearer $TL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "file_systems": [
          {
            "file_system_id": "models",
            "mount_path": "/mnt/models",
            "read_only": true,
            "prefetch": true
          }
        ]
      }'
    ```
  </Tab>
</Tabs>

### Ownership

Every mount is reachable by every user inside the sandbox — root and the image's default user alike — with ordinary file permission bits deciding access from there. What `owner` controls is **whose files they appear to be**: every file in the mount presents one owner, which is what `ls -l` shows and what non-world permission bits are checked against. In particular, on a writable mount only the presented owner (and root) can write.

By default the presented owner is the image's `tl-user` account when the image has one — Tensorlake base images do — and root otherwise. That default is what you want until you build a custom image with its own user: with files presented as root's, that user can read world-readable files (like mode-755 directories and 644 files) but cannot write. Set `owner` to fix that:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from tensorlake.sandbox import FileSystemMount, Sandbox

    sandbox = Sandbox.create(
        image="my-registry/agent-image",  # default user: agent
        file_systems=[
            FileSystemMount(
                file_system_id="workspace",
                mount_path="/workspace",
                owner="agent",
            ),
        ],
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const sandbox = await Sandbox.create({
      image: "my-registry/agent-image",
      fileSystems: [
        { fileSystemId: "workspace", mountPath: "/workspace", owner: "agent" },
      ],
    });
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    curl -X POST https://api.tensorlake.ai/sandboxes \
      -H "Authorization: Bearer $TL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "file_systems": [
          {
            "file_system_id": "workspace",
            "mount_path": "/workspace",
            "owner": "agent"
          }
        ]
      }'
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    # Runtime attach supports --owner; the -f create spec does not carry it yet,
    # so set owners at creation through the SDKs or HTTP API.
    tl sbx fs attach <sandbox-id> --id workspace --path /workspace --owner agent
    ```
  </Tab>
</Tabs>

The spec is `NAME`, `UID`, `NAME:GROUP`, or `UID:GID` — for example `agent`, `1001`, or `1001:1001`. A named user or group is resolved against the sandbox image's own user database when the mount attaches and must exist in the image; a numeric id is used as-is and needs no user database entry. `tl sbx fs ls` shows the spec as `owner=agent` in the mount's options.

Two failure modes to know about: a malformed spec (empty parts, more than one `:`, an id that does not fit 32 bits) is rejected with `400` — the SDKs and CLI reject it client-side before any request — while a well-formed **name that does not exist in the image** can only be discovered inside the guest, so it fails the sandbox after acceptance with an `error_details` message naming the unresolvable user, the same shape as mounting a nonexistent filesystem.

Like read-only and pins, owners are fail-closed: an owner-bearing mount is only placed on executor fleets that enforce it, so it can never silently present the wrong owner. On fleets that have not yet been updated, a sandbox requesting an owner-bearing mount will not be placed.

### Pinned mounts

A plain mount — read-only or not — follows the live filesystem: pushes and autosave checkpoints from anywhere become visible in the sandbox. `snapshot_id` pins the mount to one permanent snapshot instead. A pinned mount serves exactly the files captured in that snapshot and never follows the filesystem head, no matter how the filesystem advances afterward, so an entire fleet of sandboxes can mount one immutable state.

Pins reference permanent snapshots: the ones created with `tl fs snapshot` (or a message-bearing `tl fs push -m`) and listed under `Snapshots` in `tl fs history`:

```bash theme={null}
tl fs snapshot /mnt/skills -m "skills release 2026-08-20"
tl fs history skills
```

A pinned mount must also be `read_only` — the snapshot is immutable, and the server rejects a `snapshot_id` without `read_only` with `400`. The SDKs and CLI reject the combination client-side before any request is made.

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    tl sbx create -f 'skills@4b8e2d6f0a3c7e1b5d9f2a6c8e0b4d7f1a3c5e9b2d6f8a0c4e7b1d3f5a9c2e60:/skills:ro'
    ```

    The pin goes after the filesystem name as `<name>@<snapshot-id>`; quote the mount spec so the shell never splits it.
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from tensorlake.sandbox import FileSystemMount, Sandbox

    sandbox = Sandbox.create(
        file_systems=[
            FileSystemMount(
                file_system_id="skills",
                mount_path="/skills",
                read_only=True,
                snapshot_id="4b8e2d6f0a3c7e1b5d9f2a6c8e0b4d7f1a3c5e9b2d6f8a0c4e7b1d3f5a9c2e60",
            ),
        ],
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Sandbox } from "tensorlake";

    const sandbox = await Sandbox.create({
      fileSystems: [
        {
          fileSystemId: "skills",
          mountPath: "/skills",
          readOnly: true,
          snapshotId: "4b8e2d6f0a3c7e1b5d9f2a6c8e0b4d7f1a3c5e9b2d6f8a0c4e7b1d3f5a9c2e60",
        },
      ],
    });
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    curl -X POST https://api.tensorlake.ai/sandboxes \
      -H "Authorization: Bearer $TL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "file_systems": [
          {
            "file_system_id": "skills",
            "mount_path": "/skills",
            "read_only": true,
            "snapshot_id": "4b8e2d6f0a3c7e1b5d9f2a6c8e0b4d7f1a3c5e9b2d6f8a0c4e7b1d3f5a9c2e60"
          }
        ]
      }'
    ```
  </Tab>
</Tabs>

Pins work everywhere mounts do — at creation, on a [warm-pool claim](#mount-on-a-warm-pool-claim), and on [runtime attach](#attach-and-detach-at-runtime). `tl sbx fs ls` shows a pinned mount as `skills@<snapshot-id>` in the same `<name>@<snapshot-id>` syntax.

Like read-only, pinning is fail-closed: a sandbox with a pinned mount is only placed on executor fleets that enforce the pin, so it can never silently degrade to mounting the live filesystem. On fleets that have not yet been updated, a sandbox requesting a pinned mount will not be placed.

Two guardrails to know about:

* Pinning a snapshot that does not exist on the filesystem — including an ephemeral autosave id, which cannot be pinned — fails the sandbox with `FileSystemSnapshotNotFound`; see [Errors](#errors).
* Deleting a snapshot with live pinned mounts is refused: `tl fs delete-snapshot` returns a `409` naming the pin count. Terminate the sandboxes or detach the pinned mounts first.

All four options require the latest SDK/CLI and up-to-date executor fleets.

## Mount on a Warm-Pool Claim

[Pools](/sandboxes/pools) keep containers pre-booted without filesystems; mounts belong to the claim, not the pool. Pass the same `file_systems` when claiming, and the mounts are ready before the claimed sandbox is reported as running.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from tensorlake.sandbox import FileSystemMount, Sandbox

    sandbox = Sandbox.create(
        pool_id=pool.pool_id,
        file_systems=[
            FileSystemMount(file_system_id="data", mount_path="/mnt/data"),
        ],
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const sandbox = await Sandbox.create({
      poolId: pool.poolId,
      fileSystems: [{ fileSystemId: "data", mountPath: "/mnt/data" }],
    });
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    curl -X POST https://api.tensorlake.ai/sandbox-pools/<pool-id>/sandboxes \
      -H "Authorization: Bearer $TL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "file_systems": [
          {"file_system_id": "data", "mount_path": "/mnt/data"}
        ]
      }'
    ```

    The response includes `claim_configuration_applied: true`, which confirms the server decoded and persisted the claim's mounts. SDKs check this to fail closed against older servers that would accept but ignore the body.
  </Tab>
</Tabs>

## Attach and Detach at Runtime

A running sandbox can attach and detach filesystems without restarting. Attach accepts the same per-mount options as creation, including [snapshot pins](#pinned-mounts).

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    # Attach (optionally --read-only and/or --prefetch;
    # --snapshot <id> pins the mount and requires --read-only;
    # --owner <spec> presents the files as a guest user's)
    tl sbx fs attach <sandbox-id> --id data --path /mnt/data

    # List current mounts
    tl sbx fs ls <sandbox-id>

    # Detach by mount path
    tl sbx fs detach <sandbox-id> --path /mnt/data
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    sandbox.attach_file_system("data", "/mnt/data", read_only=False, prefetch=False)

    # Pin the attach to a permanent snapshot (requires read_only=True)
    sandbox.attach_file_system(
        "skills",
        "/skills",
        read_only=True,
        snapshot_id="4b8e2d6f0a3c7e1b5d9f2a6c8e0b4d7f1a3c5e9b2d6f8a0c4e7b1d3f5a9c2e60",
    )

    for mount in sandbox.list_file_systems():
        print(mount.file_system_id, mount.mount_path)

    sandbox.detach_file_system("/mnt/data")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    await sandbox.attachFileSystem("data", "/mnt/data", {
      readOnly: false,
      prefetch: false,
    });

    // Pin the attach to a permanent snapshot (requires readOnly: true)
    await sandbox.attachFileSystem("skills", "/skills", {
      readOnly: true,
      snapshotId: "4b8e2d6f0a3c7e1b5d9f2a6c8e0b4d7f1a3c5e9b2d6f8a0c4e7b1d3f5a9c2e60",
    });

    for (const mount of await sandbox.listFileSystems()) {
      console.log(mount.fileSystemId, mount.mountPath);
    }

    await sandbox.detachFileSystem("/mnt/data");
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    # Attach
    curl -X POST https://api.tensorlake.ai/sandboxes/<sandbox-id>/file_systems \
      -H "Authorization: Bearer $TL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"file_system_id": "data", "mount_path": "/mnt/data"}'

    # Detach
    curl -X DELETE https://api.tensorlake.ai/sandboxes/<sandbox-id>/file_systems \
      -H "Authorization: Bearer $TL_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"mount_path": "/mnt/data"}'
    ```

    A `200` means the change is persisted; the mount or unmount applies asynchronously on the live sandbox moments later.
  </Tab>
</Tabs>

<Warning>
  Runtime attach is fail-closed: if an attached mount later cannot converge — most commonly because the filesystem does not exist, or an `owner` names a user missing from the image — the whole sandbox is terminated, with the reason and an actionable `error_details` message on the sandbox object. Verify the filesystem exists (`tl fs ls`) before attaching to a sandbox whose state you care about, or mount at creation time instead, where the same mistake fails only the create.
</Warning>

Two other runtime-attach responses to know about:

* **`400`** — the mount is invalid (for example a `snapshot_id` without `read_only`, or a malformed `owner` spec), or the sandbox runs on an executor fleet without filesystem (or snapshot-pin, or mount-owner) support. In the fleet case, recreate the sandbox to mount filesystems.
* **`409`** — the mount path is already in use, the sandbox is not running, or the sandbox's executor is momentarily unresolvable (for example during a reconnect window). The last case is transient: retry shortly.

## Errors

Mounting a filesystem that does not exist fails sandbox creation with HTTP `422`:

```json theme={null}
{
  "sandbox_id": "sbx-...",
  "status": "failed",
  "reason": "FileSystemNotFound"
}
```

The sandbox object records the same reason with a message telling you exactly what to do:

```json theme={null}
{
  "status": "terminated",
  "termination_reason": "FileSystemNotFound",
  "error_details": "File system 'data' was not found in this project. Create it with 'tl fs create data' before mounting it at /mnt/data."
}
```

Create the filesystem first, then create the sandbox:

```bash theme={null}
tl fs create data
tl sbx create -f data:/mnt/data
```

A [pinned mount](#pinned-mounts) whose `snapshot_id` does not exist on the filesystem — or names an ephemeral autosave rather than a permanent snapshot — fails the same way, with reason `FileSystemSnapshotNotFound`:

```json theme={null}
{
  "status": "terminated",
  "termination_reason": "FileSystemSnapshotNotFound",
  "error_details": "Snapshot '4b8e2d6f0a3c7e1b5d9f2a6c8e0b4d7f1a3c5e9b2d6f8a0c4e7b1d3f5a9c2e60' was not found on file system 'skills' (only permanent snapshots created with 'tl fs snapshot' can be pinned)."
}
```

List the pinnable ids with `tl fs history skills` and pin one from its `Snapshots` section.

An [`owner`](#ownership) naming a user or group that does not exist in the sandbox image fails the same way: the spec's shape is validated up front (a malformed spec is a synchronous `400`), but the name itself can only be resolved against the image's user database inside the guest, so the sandbox fails after acceptance with an `error_details` message naming the unresolvable user. Use a numeric `UID[:GID]` spec to avoid depending on the image's user database entirely.

## Sharing Across Sandboxes

The same filesystem can be mounted by multiple sandboxes in one project concurrently. Writes from one sandbox become visible to the others as autosave checkpoints replicate — see [Concurrent Writes](/filesystems/concurrent-writes) for the merge semantics and [Distribute Files](/filesystems/distribute-files) for rolling out shared assets to a fleet of sandboxes with read-only mounts.

## Related Guides

<CardGroup cols={2}>
  <Card title="Filesystems" icon="hard-drive" href="/filesystems/introduction">
    Durable, versioned filesystems: create, push, snapshot, and time-travel.
  </Card>

  <Card title="Read-only Mounts" icon="lock" href="/filesystems/read-only-mounts">
    Pinned and following mounts for fixed inputs and shared assets.
  </Card>

  <Card title="Sandbox Pools" icon="layer-group" href="/sandboxes/pools">
    Pre-warm sandboxes and mount filesystems at claim time.
  </Card>

  <Card title="File Operations" icon="folder-open" href="/sandboxes/file-operations">
    Copy, read, and write files on the sandbox's ephemeral root disk.
  </Card>
</CardGroup>
