The virtual filesystem

Every read and write a session performs goes through one interface. That is what makes "no ambient filesystem access" true rather than aspirational, and it is where a host decides what the sandbox can see.

IFileSystem

The interface covers what a shell needs: ReadFileAsync, WriteFileAsync, AppendFileAsync, CreateDirectoryAsync, ReadDirectoryAsync, StatAsync, ExistsAsync, RenameAsync, CopyAsync, RemoveAsync, symlinks, mode and modified time. A host either uses the bundled in-memory backend or implements the interface over whatever it actually has: a database, an object store, a set of generated documents, or a projection of its own configuration.

var bash = Bash.CreateBuilder()
    .WithFileSystem(new InMemoryFileSystem())
    .WithFileSystemLimits(new FsLimits { MaxTotalBytes = 32 * 1024 * 1024 })
    .Build();

With no WithFileSystem call the session gets an empty in-memory tree, so it can read nothing until you put something in it.

Seeding it

The simplest case is to write the files from the host before handing the session over:

var fs = new InMemoryFileSystem();

await fs.CreateDirectoryAsync(VPath.Parse("/data"), recursive: true);
await fs.WriteFileAsync(VPath.Parse("/data/orders.csv"), Encoding.UTF8.GetBytes(csv));

var bash = Bash.CreateBuilder().WithFileSystem(fs).Build();

The other case, and the more interesting one, is a filesystem whose contents are generated on read. Nothing requires the tree to be stored: an implementation may synthesise a directory listing from a query and produce a file's bytes when it is opened. That is how Curiosity Workspace mounts a workspace's configuration for Sudo: /proc there is regenerated on every read, so cat run twice reports two different moments.

POSIX paths

Paths are POSIX on every host, handled by VPath rather than System.IO.Path:

  • / is the separator, on Windows too;
  • there is no drive letter and no UNC path to resolve;
  • . and .. are normalised inside the virtual root, so .. cannot climb out of it;
  • a path is a value, not a host resource, so nothing about it depends on the machine.

Quotas

The filesystem carries its own limits, separate from the execution budget:

Limit Guards against
MaxTotalBytes (100 MB) a script filling memory by writing
MaxFileBytes (50 MB) one enormous file
MaxFiles (10,000) a script filling memory by creating
MaxDepth (64) a nesting bomb, and an unbounded tree walk
MaxSymlinkHops (40) a symlink cycle
MaxNameLength (255) a pathological file name

Depth is capped where paths are created as well as where they are walked, so a host-supplied filesystem that is deeper than this sandbox would have allowed still cannot make a walk run forever.

Bytes, not strings

The unit of shell data is bytes. StreamData holds them, and UTF-8 decoding happens only at the edges, so binary content survives a pipeline intact instead of being mangled by a round trip through string.

That matters when a script does something like xxd, tar or a checksum, and it is why a custom filesystem backend should hand back bytes rather than decoded text.

Sharing it with Python

When the joined Computerwelt package is used, the Python side's open, os and os.path are backed by the same IFileSystem. A file a shell command wrote is a file the Python program reads, under the same quotas, with the same paths.

A host function that needs storage where the host configured none says so in the program's own terms: it raises a Python OSError the script can catch, rather than letting a host exception escape into it.

  • The sandbox model — why this interface is the boundary.
  • Python — how the same filesystem reaches open and os.
  • Limits — the execution budget beside these quotas.
© 2026 Curiosity. All rights reserved.