---
title: Filesystem
description: Read and write files inside a running sandbox
icon: "folder-open"
---

The filesystem API lets you read, write, and manage files inside a sandbox without mounting volumes or SSH. It uses the same channel as command execution, so it doesn't touch the sandbox's network.

Handy for pushing generated code into a sandbox, pulling back results, or inspecting logs without having to pre-mount a shared directory.

## Write a file

<CodeGroup>
```rust Rust
sb.fs().write("/app/config.json", r#"{"debug": true}"#).await?;
```

```typescript TypeScript
await sb.fs().write("/app/config.json", '{"debug": true}');
```

```python Python
await sb.fs.write("/app/config.json", b'{"debug": true}')
```

```go Go
err := sb.FS().WriteString(ctx, "/app/config.json", `{"debug": true}`)
```

</CodeGroup>

## Read a file

<CodeGroup>
```rust Rust
let content = sb.fs().read_to_string("/app/config.json").await?;
```

```typescript TypeScript
const content = await sb.fs().readToString("/app/config.json");
```

```python Python
content = await sb.fs.read_text("/app/config.json")
```

```go Go
content, err := sb.FS().ReadString(ctx, "/app/config.json")
```

</CodeGroup>

## List a directory

<CodeGroup>
```rust Rust
let entries = sb.fs().list("/app").await?;
for entry in entries {
    println!("{}: {:?}", entry.path, entry.kind);
}
```

```typescript TypeScript
const entries = await sb.fs().list("/app");
for (const entry of entries) {
    console.log(`${entry.path}: ${entry.kind}`);
}
```

```python Python
entries = await sb.fs.list("/app")
for entry in entries:
    print(f"{entry.path}: {entry.kind}")
```

```go Go
entries, err := sb.FS().List(ctx, "/app")
for _, entry := range entries {
    fmt.Printf("%s: %s\n", entry.Path, entry.Kind)
}
```

</CodeGroup>

## Stream large files

For files too large to fit in memory, use streaming. Data is transferred in chunks of approximately 3 MiB each.

<CodeGroup>
```rust Rust
let mut stream = sb.fs().read_stream("/app/data.bin").await?;
while let Some(chunk) = stream.recv().await? {
    process(&chunk);
}
```

```typescript TypeScript
const stream = await sb.fs().readStream("/app/data.bin");
for await (const chunk of stream) {
    processChunk(chunk); // chunk is a Uint8Array
}
```

```python Python
stream = await sb.fs.read_stream("/app/data.bin")
async for chunk in stream:
    process(chunk)
```

```go Go
stream, err := sb.FS().ReadStream(ctx, "/app/data.bin")
for {
    chunk, err := stream.Recv(ctx)
    if err != nil {
        return err
    }
    if chunk == nil {
        break
    }
    process(chunk)
}
```

</CodeGroup>

## Copy from host

Copy a file from the host machine into the sandbox in a single call.

<CodeGroup>
```rust Rust
sb.fs().copy_from_host("./local-file.txt", "/app/remote-file.txt").await?;
```

```typescript TypeScript
await sb.fs().copyFromHost("./local-file.txt", "/app/remote-file.txt");
```

```python Python
await sb.fs.copy_from_host("./local-file.txt", "/app/remote-file.txt")
```

```go Go
err := sb.FS().CopyFromHost(ctx, "./local-file.txt", "/app/remote-file.txt")
```

</CodeGroup>

<Tip>
  If you need to transfer many files at once, consider using a [bind-mounted volume](/sandboxes/volumes) instead. Volumes give the guest direct filesystem access, whereas the filesystem API transfers each file individually. For bulk operations, volumes are significantly faster.
</Tip>

## Extensible backends

The default filesystem backends handle most use cases. For advanced scenarios, you can attach hooks to intercept operations on a volume, or implement a full custom backend.

### Hooks <sup><sup>coming soon</sup></sup>

Intercept file reads and writes on a volume without replacing the entire backend. Hooks receive the path and data, and return transformed data. The underlying filesystem handles everything else (permissions, directories, metadata).

<CodeGroup>
```rust Rust
use microsandbox::Sandbox;

let key = get_encryption_key();
let sb = Sandbox::builder("encrypted")
    .image("python")
    .volume("/secrets", |v| v
        .bind("/data/secrets")
        .on_read(move |_path, data| decrypt(data, &key))
        .on_write(move |_path, data| encrypt(data, &key))
    )
    .create().await?;
```

```typescript TypeScript
import { Sandbox } from "microsandbox";

// Hooks are coming soon. The planned shape will sit alongside the regular
// mount methods on the volume builder, e.g.:
//
//   .volume("/secrets", (m) => m
//       .bind("/data/secrets")
//       .onRead((path, data) => decrypt(data, key))
//       .onWrite((path, data) => encrypt(data, key)),
//   )
//
// For now the closest equivalent is a plain bind:
await using sb = await Sandbox.builder("encrypted")
    .image("python")
    .volume("/secrets", (m) => m.bind("/data/secrets"))
    .create();
```

```python Python
from microsandbox import Sandbox, Volume

key = get_encryption_key()
sb = await Sandbox.create(
    "encrypted",
    image="python",
    volumes={
        "/secrets": Volume.bind("/data/secrets",
            on_read=lambda path, data: decrypt(data, key),
            on_write=lambda path, data: encrypt(data, key),
        ),
    },
)
```

```go Go
// Mount the host directory directly.
sb, err := m.CreateSandbox(ctx, "encrypted",
    m.WithImage("python"),
    m.WithMounts(map[string]m.MountConfig{
        "/secrets": m.Mount.Bind("/data/secrets", m.MountOptions{}),
    }),
)
```

</CodeGroup>

### Custom backend <sup><sup>coming soon</sup></sup>

For full control, implement the `FsBackend` trait (Rust) or class (TypeScript). This gives you access to every POSIX operation: `read`, `write`, `lookup`, `getattr`, `readdir`, etc. You can delegate to a built-in backend (like `PassthroughFs`) for operations you don't need to customize.

<CodeGroup>
```rust Rust
use microsandbox::{FsBackend, PassthroughFs};
use std::io;

struct EncryptedFs { key: [u8; 32], inner: PassthroughFs }

impl FsBackend for EncryptedFs {
    fn read(&self, ctx: Context, inode: u64, handle: u64,
            buf: &mut [u8], offset: u64) -> io::Result<usize> {
        let n = self.inner.read(ctx, inode, handle, buf, offset)?;
        self.decrypt_in_place(&mut buf[..n]);
        Ok(n)
    }
    fn write(&self, ctx: Context, inode: u64, handle: u64,
             buf: &[u8], offset: u64) -> io::Result<usize> {
        let encrypted = self.encrypt(buf);
        self.inner.write(ctx, inode, handle, &encrypted, offset)
    }
    // ... remaining methods delegate to self.inner
}

let sb = Sandbox::builder("custom")
    .volume("/secrets", |v| v.backend(EncryptedFs::new(key, "/data")?))
    .create().await?;
```

```typescript TypeScript
// FsBackend is coming soon. The planned API will let you plug a custom
// backend into a volume:
//
//   class EncryptedFs implements FsBackend {
//     // ... implement read, write, and other required methods
//   }
//
//   const sb = await Sandbox.builder("custom")
//     .volume("/secrets", (m) => m.backend(new EncryptedFs(key, "/data")))
//     .create();
```

```python Python
from microsandbox import FsBackend, Sandbox, Volume

# Implement the FsBackend interface
class EncryptedFs(FsBackend):
    # ... implement read, write, and other required methods
    pass

sb = await Sandbox.create(
    "custom",
    volumes={
        "/secrets": Volume.backend(EncryptedFs(key, "/data")),
    },
)
```

```go Go
// Custom filesystem backends are not available in the current SDKs yet.
// Use bind mounts, named volumes, or tmpfs mounts.
```

</CodeGroup>
