sandfs
sandfs is a sandbox filesystem for agents. Mount any data without copying it (a folder, in-memory files, an API, a bucket, a git commit), let the agent read and write through a copy-on-write workspace, fork that workspace in memory in O(1) for every attempt, and collect the files it changed. Files are bytes of any kind: documents, spreadsheets, images, data sets and code alike.
Status: early. The core is Rust (
crates/), and the Python package is a thin binding over it. APIs may still change.
Primitives
- Source: a read-only tree, read only when a path below it is touched. sandfs ships a host directory, in-memory files and an optional git source; anything else is two methods you write,
listandread. - Workspace: sources mounted at paths, plus a copy-on-write overlay that holds every write, directory and deletion. Sources are never modified.
- Change: one file that differs from the mounts, returned by
changes().
Everything else is a composition. A snapshot is a fork nobody writes to, a working copy on disk is a fork mounted over FUSE, and saving is the harness walking changes() and writing each changed file wherever its data lives.
Installation
pip install "sandfs>=0.4" # prebuilt wheels for Linux, macOS and Windows, Python 3.11+
cargo add sandfs-core # the Rust core; sandfs-git adds the git source
cargo install sandfs-mcp # an MCP server with file tools over a workspace
Until 0.4.0 is published, install from git instead with pip install git+https://github.com/lydakis/sandfs.git, which builds the extension and needs a Rust toolchain.
Quickstart
import sandfs
class Tickets:
"""Any data can be a source: list a directory, read a file."""
def list(self, path):
return [sandfs.DirEntry("T-1.json"), sandfs.DirEntry("T-2.json")]
def read(self, path):
return fetch_ticket_json(path.removeprefix("/").removesuffix(".json"))
base = sandfs.Workspace()
base.mount("/reports", sandfs.DirSource("./reports")) # a host folder, never written to
base.mount("/tickets", Tickets()) # read only when the agent looks
base.mount("/brief", sandfs.MemorySource({"task.md": "Summarise open tickets\n"}))
attempt = base.fork() # one fork per agent or attempt
version = attempt.write("/reports/summary.md", "# Open tickets\n")
attempt.write("/reports/summary.md", "# Open tickets (2)\n", expected_version=version)
attempt.write("/reports/chart.png", png_bytes)
for change in attempt.changes():
print(change.kind, change.path) # added /reports/chart.png, added /reports/summary.md
base is unchanged: forks share the mounts and everything already read, but never see each other's changes.
A source must return the same answer every time, because what it returns is cached and shared between forks. Sizes in DirEntry are optional; stat reads a file once to find a size the source left out.
Large files can be read in pieces: read(path, offset=..., length=...) asks the source for just that range and doesn't cache it, so a multi-gigabyte data set or video is never held in memory whole. DirSource seeks on disk, and a Python source can define read_range(path, offset, length); without it, the range is sliced from a whole read.
sandfs.GitSource(repo, rev="HEAD") mounts a commit of a local repository without a checkout. In a partial clone (git clone --filter=blob:none), git fetches a file's contents only when it is first read.
API
| Call | Does |
|---|---|
mount(path, source) |
Mount a DirSource(root), MemorySource({name: str or bytes}), GitSource(repo, rev), or any object with list(path) and read(path) |
fork() |
A new workspace with the same mounts and changes, in O(1) |
read(path, *, offset=0, length=None), read_text(path) |
File contents as bytes (all of it, or one range), or as UTF-8 text |
list(path="/"), stat(path), exists(path) |
Entries (name, kind, size), and kind, size and version |
write(path, data, *, expected_version=None) |
Write str or bytes, creating parents, and return the new version |
mkdir, remove, copy, move |
File operations; base files are copied by reference |
changes() |
Change(path, kind) for every added, modified or deleted file |
Errors are built-in exceptions (FileNotFoundError, FileExistsError, IsADirectoryError, NotADirectoryError, ValueError, OSError) plus sandfs.VersionMismatch, which carries path, expected and actual. An exception raised inside a Python source surfaces as OSError. Calls into the core release the GIL, so a slow source never blocks other threads. A workspace is not meant to be shared between threads; fork one per thread instead.
Mounting a workspace on disk
The sandfs command (crate sandfs-cli) mounts a workspace as a real directory with FUSE, so any program can read and write it: a data pipeline, a notebook, a build, an agent's tools. The folder underneath is never modified, and forking never copies it. It mounts on Linux with FUSE 3 (fusermount3, package fuse3). On macOS and Windows it does everything but mount, and sandfs serve --mcp SOURCE creates workspaces there.
cargo install --path crates/sandfs-cli
sandfs mount ~/datasets/survey ~/ws/survey # a host folder, never written to
sandfs fork ~/ws/survey ~/ws/cleanup # survey's changes so far, without copying the folder
python clean.py ~/ws/cleanup # real files, any tool
sandfs changes ~/ws/cleanup # A/M/D against the source
sandfs unmount ~/ws/cleanup # changes stay saved
sandfs mount cleanup ~/ws/cleanup # and come back
sandfs serve --mcp NAME gives an unmounted workspace to an agent as MCP file tools instead, with the source's files under /mnt; whatever the agent changes is saved for the next mount or changes. One process serves a workspace at a time: a second mount or MCP session is refused while the first runs, so parallel attempts each get their own fork.
Changes live in memory and are saved under $SANDFS_HOME (default ~/.local/share/sandfs) shortly after each write. A fork on disk costs more than a fork in memory. sandfs fork never copies the source, but it waits for the workspace's latest changes to be saved, links each changed file into the fork (copying it where the disk can't link), and starts a new mount, which reads the source again rather than sharing what the first mount has read. git:<path or url>[@rev] mounts a commit instead of a folder; for a remote repository sandfs fetches only that commit and its folder listings, which takes about a second even for a large repository, and fetches a directory's files in one request when it is first listed. Offline, a commit id that was mounted before still mounts and the folders opened before still read, but any other folder needs the network. A branch or tag name is looked up on the server every time, so it doesn't mount offline.
What sandfs is not
Not an execution sandbox. sandfs does not run commands, and it does not contain the programs that use it. It keeps their writes out of the source, but a program working in a FUSE mount runs with its own process's access and can still read or change anything else its user can. Run untrusted programs in a real sandbox (a container or microVM) and point it at the mount.
Not a snapshot of a host folder. A host folder is read when a path below it is first touched. Directory listings and whole-file reads are kept from then on, but ranged reads (which a FUSE mount uses for files over 64 MiB) go back to the folder every time. If the folder changes while it is mounted, a workspace can show a mix of old and new: a listing or size read before the change next to contents read after it, or two ranges of one file from different versions. Mount a folder nothing else writes to, copy it first, or mount a git commit when the view must not change.
sandfs also has no symlinks and no text-editing or patch tools in the core; those are code concerns that live in the MCP tools, if anywhere. Earlier versions had a bash-like shell, a host-command bridge, a Python executor, access policies and storage adapters. They were dropped to keep the primitive small.
Repository layout
.
├── crates/
│ ├── sandfs-core/ # workspaces, overlay, changes, directory and in-memory sources
│ ├── sandfs-git/ # optional lazy git source
│ ├── sandfs-cli/ # the sandfs command: mount, fork and changes over FUSE
│ ├── sandfs-mcp/ # MCP server: file tools over one workspace
│ └── sandfs-py/ # Python bindings (the sandfs._core extension)
├── docs/ # roadmap, changelog and publishing notes
├── sandfs/ # Python package: re-exports and type stubs
└── tests/ # Python tests
Local development
Building the package compiles the Rust extension, so you need a Rust toolchain (rustup). Re-run the install after changing Rust code.
python -m venv .venv
source .venv/bin/activate
uv pip install -e .[dev]
pre-commit install
uv run pytest
uv run ruff check .
uv run mypy sandfs
cargo test --workspace
License
MIT
Further reading
Metadata
Release files for sandfs 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| sandfs-0.4.0.tar.gz | 47.8 kB | With provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| sandfs-0.4.0-cp311-abi3-win_amd64.whl | CPython 3.11 | abi3 | Windows x86-64 | Details |
| sandfs-0.4.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| sandfs-0.4.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.11 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| sandfs-0.4.0-cp311-abi3-macosx_11_0_arm64.whl | CPython 3.11 | abi3 | macOS 11.0+ ARM64 | Details |
| sandfs-0.4.0-cp311-abi3-macosx_10_12_x86_64.whl | CPython 3.11 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 2.3 MB
Release files / sandfs-0.4.0.tar.gz
| Download URL | sandfs-0.4.0.tar.gz |
|---|---|
| Size | 47.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ac4d12d84863cadcac1b1fe69427ca931a06ffc89bf438408c959bde04a8b2f9
|
|
BLAKE2b-256 checksum How to use checksums |
0b47a9cec307c120473a286cdf4e552ede3a04f1a75cb4bd8646fddc0445bab2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / sandfs-0.4.0-cp311-abi3-win_amd64.whl
| Download URL | sandfs-0.4.0-cp311-abi3-win_amd64.whl |
|---|---|
| Size | 366.1 kB |
| Tags | CPython 3.11 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
1dc204df226eaf1fe4d9d88dbe5005693dbb5bd4381e21330ecfa1bd9df265c4
|
|
BLAKE2b-256 checksum How to use checksums |
bbf8faba9110fe7fadc4f63dcc2d50e4ce1b2c09248a18c6215cad86d9629c3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / sandfs-0.4.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | sandfs-0.4.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 504.2 kB |
| Tags | CPython 3.11 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
6b2e57cc67e6ef4398801d8e5e7f2fdc7528671b248b5b3fcfed806d41d8672b
|
|
BLAKE2b-256 checksum How to use checksums |
f216d1e38122a54b6b858ef698551ce02cfd57ba6a60bd90b607338c70b42614
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / sandfs-0.4.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | sandfs-0.4.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 494.5 kB |
| Tags | CPython 3.11 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
13f24f768003c48721bfea22fea63478ef41ce86f0e330bb20fdd6e1da541e9e
|
|
BLAKE2b-256 checksum How to use checksums |
1075d04a1e7ff65ecbaf089f845a498d2aa9f62eaf955db31bc2658e70999a1b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / sandfs-0.4.0-cp311-abi3-macosx_11_0_arm64.whl
| Download URL | sandfs-0.4.0-cp311-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 449.0 kB |
| Tags | CPython 3.11 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
845fca84439e315b74a351a17d360ec250495003895127b91616b1d7cff656e6
|
|
BLAKE2b-256 checksum How to use checksums |
a4f0ad9a274f38195cd0f7c3c7afff5ce165f29c0674ceb29e3587c9d3577aad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / sandfs-0.4.0-cp311-abi3-macosx_10_12_x86_64.whl
| Download URL | sandfs-0.4.0-cp311-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 459.6 kB |
| Tags | CPython 3.11 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
c38eab6dc597c18ba41f92c4362f0d831807a2adbff81ac35d65c3662bfc28e3
|
|
BLAKE2b-256 checksum How to use checksums |
1a3bceb15ef66c34446497155191d3fc3abba4c86aab817d2f704b856cbf6fda
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for sandfs 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 6 files in this release have been uploaded with provenance metadata.
Attestation for sandfs-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.