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

# Custom VFS

> Mount a service Mirage does not ship by writing your own VFS in Python or TypeScript.

A VFS is a class that extends `BaseVFS`. Override `readdir`, `stat` and
`read`, and `ls`, `cat`, `grep`, `find` and the other read commands work on
your service. Every other method is optional: override one, and the commands
that need it start working, or answer with fewer requests.

## The base class

The methods you can override, with the arguments you normally use:

**Python**

```python theme={null}
class BaseVFS:
    name: str
    prompt: str
    write_prompt: str

    async def readdir(self, path: PathSpec) -> list[FileStat]: ...
    async def stat(self, path: PathSpec) -> FileStat: ...
    async def read(self, path: PathSpec, *, offset: int = 0, size: int | None = None) -> bytes: ...

    async def write(self, path: PathSpec, data: bytes) -> FileStat | None: ...
    async def append(self, path: PathSpec, data: bytes) -> FileStat | None: ...
    async def copy(self, src: PathSpec, dst: PathSpec) -> FileStat | None: ...
    async def truncate(self, path: PathSpec, length: int, no_create: bool = False) -> FileStat | None: ...
    async def rename(self, src: PathSpec, dst: PathSpec) -> FileStat | None: ...
    async def unlink(self, path: PathSpec) -> None: ...
    async def mkdir(self, path: PathSpec, parents: bool = False) -> FileStat | None: ...
    async def rmdir(self, path: PathSpec) -> None: ...
    async def rm_r(self, path: PathSpec) -> None: ...
    async def dir_copy(self, src: PathSpec, dst: PathSpec) -> FileStat | None: ...
    async def setattr(self, path: PathSpec, *, mode: int | None = None, uid: int | str | None = None, gid: int | str | None = None, atime: str | None = None, mtime: str | None = None) -> dict[str, int | str]: ...

    async def files_containing(self, path: PathSpec, text: str, *, whole_word: bool, ignore_case: bool) -> list[PathSpec] | None: ...
    async def lines_containing(self, path: PathSpec, text: str, *, ignore_case: bool) -> bytes | None: ...
    async def first_lines(self, path: PathSpec, n: int) -> bytes | None: ...
    async def last_lines(self, path: PathSpec, n: int) -> bytes | None: ...
    async def line_count(self, path: PathSpec) -> int | None: ...
    async def follow(self, path: PathSpec, n: int, interval: float) -> AsyncIterator[bytes] | None: ...
    async def checksum(self, path: PathSpec, algorithm: str) -> str | None: ...
    async def read_tree(self, path: PathSpec) -> dict[str, FileStat] | None: ...

    async def close(self) -> None: ...
```

**TypeScript**

```typescript theme={null}
declare class BaseVFS {
  name: string
  prompt: string
  writePrompt: string

  readdir(path: PathSpec): Promise<FileStat[]>
  stat(path: PathSpec): Promise<FileStat>
  read(path: PathSpec): Promise<Uint8Array>

  write(path: PathSpec, data: Uint8Array): Promise<unknown>
  append(path: PathSpec, data: Uint8Array): Promise<unknown>
  copy(src: PathSpec, dst: PathSpec): Promise<unknown>
  truncate(path: PathSpec, length: number, noCreate?: boolean): Promise<unknown>
  rename(src: PathSpec, dst: PathSpec): Promise<unknown>
  unlink(path: PathSpec): Promise<void>
  mkdir(path: PathSpec, parents?: boolean): Promise<unknown>
  rmdir(path: PathSpec): Promise<void>
  rmR(path: PathSpec): Promise<void>
  dirCopy(src: PathSpec, dst: PathSpec): Promise<unknown>
  setattr(path: PathSpec, fields: SetAttrFields): Promise<Record<string, number | string>>

  filesContaining(path: PathSpec, text: string, opts: { wholeWord: boolean; ignoreCase: boolean }): Promise<PathSpec[] | null>
  linesContaining(path: PathSpec, text: string, opts: { ignoreCase: boolean }): Promise<Uint8Array | null>
  firstLines(path: PathSpec, n: number): Promise<Uint8Array | null>
  lastLines(path: PathSpec, n: number): Promise<Uint8Array | null>
  lineCount(path: PathSpec): Promise<number | null>
  follow(path: PathSpec, n: number, interval: number): Promise<AsyncIterable<Uint8Array> | null>
  checksum(path: PathSpec, algorithm: string): Promise<string | null>
  readTree(path: PathSpec): Promise<Map<string, FileStat> | null>

  close(): Promise<void>
}
```

## How to integrate your service

### Required

| Method | Used by |
| - | - |
| `readdir` | `ls`, `find`, `tree`, globs |
| `stat` | every command |
| `read` | `cat`, `grep`, `head`, `wc` and every other command that reads |

### Write

Writes need a writable mount: `MountMode.WRITE`, or `mode: write` in YAML.

| Method | With it | Without it |
| - | - | - |
| `write` | `>`, `tee`, `touch` and `sed -i` work. | They fail. |
| `append` | `>>` calls it. | `>>` reads the file and calls `write`. |
| `copy` | `cp` calls it. | `cp` reads the file and calls `write`. |
| `truncate` | `truncate` calls it. | `truncate` calls `write`, and `truncate -c` fails with `Operation not supported`. |
| `rename` | `mv` works. | `mv` fails with `Operation not supported`. |
| `unlink` | `rm` works. | `rm` fails with `Operation not supported`. |
| `mkdir` | `mkdir` works. | `mkdir` fails with `Operation not supported`. |
| `rmdir` | `rmdir` works. | `rmdir` fails with `Operation not supported`. |
| `rm_r` | `rm -r` calls it once. | `rm -r` calls `unlink` and `rmdir` for each entry. |
| `dir_copy` | `cp -r` calls it once when it copies a folder to a new place and `copy` is overridden too. | `cp -r` calls `mkdir` for each folder and `copy`, or a read and `write`, for each file. |
| `setattr` | `chmod` and `touch -d` call it. | Mirage keeps the mode and times itself. |

### Answer commands with your service

Override these to answer from your service's own search, query or metadata API
instead of reading files. Each links to its guide.

| Method | With it | Without it |
| - | - | - |
| [`files_containing`](/home/integration/search) | `grep -r` and `rg` read only the files it names. | They read every file. |
| [`lines_containing`](/home/integration/search) | `grep` and `rg` match only the lines it returns. | They read the file. |
| [`first_lines`, `last_lines`](/home/integration/lines) | `head` and `tail` do not read the file. | They read the file. |
| [`line_count`](/home/integration/lines) | `wc -l` reads nothing. | `wc -l` reads the file. |
| [`follow`](/home/integration/lines) | `tail -f` waits on it. | `tail -f` re-reads the file every second. |
| [`checksum`](/home/integration/checksums) | `md5sum` and `sha256sum` read nothing. | They read the file and hash it. |
| [`read_tree`](/home/integration/walks) | `find`, `du`, `ls -R` and `tree` call it once. | They list each folder. |

### Attributes

| Attribute | Does |
| - | - |
| `name` | Names the VFS. |
| `prompt` | Describes the mount to the agent in its VFS.md. `{prefix}` becomes the mount's folder. |
| `write_prompt` | Added to the description when the mount is writable. |

Set them as class attributes or pass them to `super().__init__()`
(`super({...})` in TypeScript).

## Example: a read-only VFS

This VFS serves pages from a dictionary:

**Python**

```python notes.py theme={null}
from mirage import BaseVFS, FileStat, FileType, PathSpec


class NotesVFS(BaseVFS):
    def __init__(self, pages: dict[str, str]) -> None:
        super().__init__(name="notes")
        self.pages = pages

    def page(self, path: PathSpec) -> str:
        if path.vfs_path not in self.pages:
            raise FileNotFoundError(path.virtual)
        return self.pages[path.vfs_path]

    async def readdir(self, path: PathSpec) -> list[FileStat]:
        if path.vfs_path:
            self.page(path)
            raise NotADirectoryError(path.virtual)
        return [FileStat(name=name, type=FileType.FILE) for name in self.pages]

    async def stat(self, path: PathSpec) -> FileStat:
        if not path.vfs_path:
            return FileStat(name="/", type=FileType.DIRECTORY)
        self.page(path)
        return FileStat(name=path.vfs_path, type=FileType.FILE)

    async def read(
        self, path: PathSpec, *, offset: int = 0, size: int | None = None
    ) -> bytes:
        return self.page(path).encode()
```

**TypeScript**

```typescript notes.ts theme={null}
import { BaseVFS, enoent, enotdir, FileStat, FileType, type PathSpec } from '@struktoai/mirage-node'

export class NotesVFS extends BaseVFS {
  readonly pages: Record<string, string>

  constructor(config: { pages: Record<string, string> }) {
    super({ name: 'notes' })
    this.pages = config.pages
  }

  page(path: PathSpec): string {
    if (!Object.hasOwn(this.pages, path.vfsPath)) throw enoent(path)
    return this.pages[path.vfsPath]!
  }

  override async readdir(path: PathSpec): Promise<FileStat[]> {
    if (path.vfsPath !== '') {
      this.page(path)
      throw enotdir(path)
    }
    return Object.keys(this.pages).map((name) => new FileStat({ name, type: FileType.FILE }))
  }

  override async stat(path: PathSpec): Promise<FileStat> {
    if (path.vfsPath === '') return new FileStat({ name: '/', type: FileType.DIRECTORY })
    this.page(path)
    return new FileStat({ name: path.vfsPath, type: FileType.FILE })
  }

  override async read(path: PathSpec): Promise<Uint8Array> {
    return new TextEncoder().encode(this.page(path))
  }
}
```

Mount it like any other VFS:

**Python**

```python theme={null}
from mirage import Workspace

from notes import NotesVFS

ws = Workspace({"/notes": NotesVFS({"hello.txt": "hello mirage\n"})})
```

**TypeScript**

```typescript theme={null}
import { Workspace } from '@struktoai/mirage-node'
import { NotesVFS } from './notes.ts'

const ws = new Workspace({ '/notes': new NotesVFS({ pages: { 'hello.txt': 'hello mirage\n' } }) })
```

**YAML**

```yaml theme={null}
mounts:
  /notes:
    vfs: ./notes.py:NotesVFS
    config:
      pages:
        hello.txt: "hello mirage\n"
```

`cat /notes/hello.txt` then prints `hello mirage`, and `grep -rn mirage /notes`
reads every page. In YAML, `vfs` names the class as `file:Class`, relative to
the YAML file, and `config` is what its constructor gets; the TypeScript server
takes `./notes.ts:NotesVFS`. To name it `vfs: notes` instead, register it with
`register_vfs` in Python or `registerVfs` in TypeScript.

## Example: make it writable

Override `write` and `unlink`:

**Python**

```python theme={null}
from mirage import PathSpec

from notes import NotesVFS


class WritableNotesVFS(NotesVFS):
    async def write(self, path: PathSpec, data: bytes) -> None:
        self.pages[path.vfs_path] = data.decode()

    async def unlink(self, path: PathSpec) -> None:
        del self.pages[path.vfs_path]
```

**TypeScript**

```typescript theme={null}
import type { PathSpec } from '@struktoai/mirage-node'
import { NotesVFS } from './notes.ts'

export class WritableNotesVFS extends NotesVFS {
  override async write(path: PathSpec, data: Uint8Array): Promise<void> {
    this.pages[path.vfsPath] = new TextDecoder().decode(data)
  }

  override async unlink(path: PathSpec): Promise<void> {
    delete this.pages[path.vfsPath]
  }
}
```

On a writable mount, `echo hi > /notes/new.txt`, `sed -i` and `rm` now work,
and `>>` reads the page and writes it back. `mv` still answers
`Operation not supported` until you override `rename`.

## Example: let grep use your search

Override `files_containing` to name the files that may contain the text:

**Python**

```python theme={null}
from mirage import PathSpec
from mirage.utils.key_prefix import mounted_path

from notes import NotesVFS


class SearchableNotesVFS(NotesVFS):
    async def files_containing(
        self, path: PathSpec, text: str, *, whole_word: bool, ignore_case: bool
    ) -> list[PathSpec] | None:
        if ignore_case:
            return None
        return [
            mounted_path(path, name)
            for name, page in self.pages.items()
            if text in page
        ]
```

**TypeScript**

```typescript theme={null}
import type { PathSpec } from '@struktoai/mirage-node'
import { mountedPath } from '@struktoai/mirage-core/utils/key_prefix'
import { NotesVFS } from './notes.ts'

export class SearchableNotesVFS extends NotesVFS {
  override async filesContaining(
    path: PathSpec,
    text: string,
    opts: { wholeWord: boolean; ignoreCase: boolean },
  ): Promise<PathSpec[] | null> {
    if (opts.ignoreCase) return null
    return Object.entries(this.pages)
      .filter(([, page]) => page.includes(text))
      .map(([name]) => mountedPath(path, name))
  }
}
```

`grep -rn mirage /notes` now reads only the pages this returns, and Mirage
still checks each line, so the output is the same. The dictionary stands in
for your service's search API. `mounted_path` turns a name inside the mount
into a path. `grep -ri` gets `None` here, so it reads every page.

For a real one, see [Chroma](/python/vfs/chroma#exact-search-with-grep), the
search behind a RAG knowledge base: its `files_containing` asks Chroma for the
chunks whose text contains the word (`$contains`) and returns their pages, and
it also returns `None` for `-i`. Source:
[Python](https://github.com/strukto-ai/mirage/blob/main/python/mirage/core/chroma/search.py),
[TypeScript](https://github.com/strukto-ai/mirage/blob/main/typescript/packages/core/src/core/chroma/search.ts).

## Example: count lines without reading

Override `line_count`:

**Python**

```python theme={null}
from mirage import PathSpec

from notes import NotesVFS


class CountedNotesVFS(NotesVFS):
    async def line_count(self, path: PathSpec) -> int | None:
        return self.page(path).count("\n")
```

**TypeScript**

```typescript theme={null}
import type { PathSpec } from '@struktoai/mirage-node'
import { NotesVFS } from './notes.ts'

export class CountedNotesVFS extends NotesVFS {
  override async lineCount(path: PathSpec): Promise<number | null> {
    return this.page(path).split('\n').length - 1
  }
}
```

`wc -l /notes/hello.txt` now answers without reading the page. For a database,
this is where a `count(*)` query goes. Plain `wc` still reads the page, since it
also counts words and bytes.

## Next

The [Python](/python/vfs/new) and [TypeScript](/typescript/vfs/new) guides add
folders, snapshots, packaging and contributing a VFS to Mirage.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.