Documentation

How the layers, snapshots and checkpoints fit together, and what every command does. For measured performance, see benchmarks.

Before you start

$ sudo apt install -y build-essential pkg-config libfuse3-dev fuse3 rsync
$ cargo build --release

Fedora: dnf install fuse3-devel fuse3 rsync openssh-clients. Arch: pacman -S fuse3 rsync openssh. One binary, no daemon. It runs rootless — no sudo for normal use.

Ubuntu 24.04 and later restrict unprivileged user namespaces, which rootless mode needs. Two sysctls turn them back on: kernel.apparmor_restrict_unprivileged_userns=0 and kernel.unprivileged_userns_apparmor_policy=0. Neither is agentfs-specific — podman and rootless docker want the same. Put them in /etc/sysctl.d/ to survive a reboot.

The three layers

Everything else follows from this. A project has a lower layer — the host directory you attached, which is never written to — and an upper layer holding every change. The agent sees a merged view of the two and cannot tell them apart.

  merged view   what the agent sees, at the project's real pathupper        every write, every delete marker — snapshot this
      ↑
   lower        your directory, mounted read-only

A file is copied into upper the first time it is written — untouched files cost nothing. A deleted file can't be removed from a read-only lower, so upper records a delete marker instead; the merged view then hides it. Same for a directory replaced wholesale.

The consequence worth internalising: doing nothing is a complete rollback. Throw the upper away and the project is what it was. apply is the only command that writes to your directory.

Where state lives

One directory per project under ~/.agentfs/, and nothing outside it except what you explicitly bind.

~/.agentfs/<project>/
  upper/        the live layer — every change the agent has made
  store/
    objects/    content-addressed blobs, trees and commits (blake3 + zstd)
    refs/       name → snapshot
  HEAD          where you are in the history
  config.toml   mount source, ignore patterns, backend dialect

Snapshots are content-addressed, so an unchanged file is stored once however many snapshots contain it. ~/.agentfs is masked from inside the sandbox — the agent cannot see or edit its own history.

Ignore patterns

config.toml carries an ignore list that excludes build output — target, node_modules, dist, build, __pycache__, .venv and friends. Ignored paths are skipped at snapshot time only: the agent still sees and writes them normally, they just don't enter the history. That's what keeps snapshots fast and small.

Snapshots and HEAD

A snapshot records the whole upper as one unit. HEAD points at where you are. History is append-only: restoring to an older snapshot adds a commit rather than discarding the newer one, so you can always get back to the state you moved away from.

Three kinds of snapshot share the store and are pruned differently:

KindCreated byRetention
manualsnap, with a name you choosekept until you gc
auto-<ts>sandbox exitkept until you gc
ckpt-<class>-<ts>the watcher, during a runself-pruning, newest kept

Checkpoints prune themselves so a long session doesn't accumulate thousands; manual snapshots and exit snapshots never do, which is why they're the ones to reach for when you want a named point to return to.

Mount backends

Two things can serve the merged view. --mount-backend picks; the default is auto.

kernel overlayfsFUSE
speedat parity with no sandbox~2.2× on an incremental build
where it mountsinside the sandboxhost-side
needskernel ≥ 5.11, and ~/.agentfs not itself on overlayfslibfuse3
delete markercharacter device 0:0.wh.<name> file

auto decides by attempting a real mount in a throwaway namespace, not by guessing from the environment. "Am I in a container?" would be the wrong question in both directions: default Docker can't mount overlayfs at all, and a plain rootless host can.

A project's upper is written in one dialect. The two delete markers aren't interchangeable in place, so agentfs records which one a project uses and refuses a backend that would misread it. Snapshots themselves are dialect-free — to switch, snap and restore into a fresh project. --show-binds prints which backend was chosen and, if it fell back, why.

What's visible inside

The sandbox builds a fresh root: system directories bound read-only, fresh /proc, /sys, /tmp, and your project at its real path. Your home is handled by [home].mode in ~/.agentfs/defaults.toml:

modeEffect
fullDefault. Whole $HOME read-write, with credential paths masked. Dotfiles and tools work with no configuration.
full-roSame view, bound read-only. Blocks the "agent deletes my home" class outright; paths that must stay writable get their own overlay.
allowlistNo home bind at all. You declare every path. For an agent you don't trust.

Masked paths — SSH keys, cloud credentials, shell history, ~/.agentfs itself — appear as empty directories or empty files rather than being absent, so tools that probe them get an answer instead of an error. Add extra paths with --ro-bind and --bind.

Command reference

agentfs quick <dir> [-- <cmd>]

Init, attach and run, deriving the project name from the directory's basename. Reuses an existing project for that directory rather than making a second one. This is the command you want almost always.

$ agentfs quick ~/code/api              # a shell inside
$ agentfs quick ~/code/api -- claude    # run an agent
$ agentfs quick ~/code/api --name api-2 # a second, independent project

agentfs init <name> · agentfs mount <name> <dir>

The explicit two-step form. init creates the project; mount attaches a host directory as its lower layer. One mount source per project.

agentfs run <name> [-- <cmd>]

Enter the sandbox. Defaults to a shell; the prompt gains a [project] prefix so you can tell you're inside. The command's exit code is propagated.

FlagEffect
--mount-backendauto (default), kernel, or fuse.
--snapshotsall (default), checkpoints, exit, none.
--checkpoint-interval <s>Ceiling between automatic checkpoints. Default 90.
--ro-bind <path>Expose an extra path read-only. Repeatable.
--bind <path>Read-write. Writes leak to the host and are not snapshotted.
--preset <name>Apply a named bind set from defaults.toml.
--show-bindsPrint the resolved mount set and the chosen backend, then run.
--cwd <path>Working directory inside the sandbox.

agentfs snap <name> [tag] [-m <msg>]

Snapshot the upper. With a tag it becomes a name you can restore by; without one it's a detached hash. Safe to run while a sandbox is live — the process tree is frozen for the walk, so nothing is caught mid-write.

agentfs list <name> · agentfs log <name>

list shows every snapshot with the current one marked; log walks the history back from where you are.

agentfs diff <name> <a> <b>

Path-level diff between two snapshots — added, modified, deleted. Both refs are required.

agentfs restore <name> <ref> · agentfs undo <name>

restore puts the upper back to a snapshot; undo is one step back without naming one. Both are append-only: the state you left is still in the history, so an undo can itself be undone.

agentfs apply <name>

The only command that writes to your directory. Promotes the current state of the upper into the real tree: modified files overwritten, new files created, deleted files removed. Applies the current state, so restore first if you want an older one. Files identical in both are left alone, which makes a second apply a no-op.

agentfs status [name]

Position, whether a sandbox is live, mount sources, and how big the upper has grown. With no name, summarises every project.

agentfs gc <name>

Delete objects no snapshot reaches any more. Roots are every ref plus the current HEAD. --dry-run reports without deleting.

agentfs checkpoint

Run from inside a sandbox to ask for a checkpoint now. Blocks until one exists, so a caller returns knowing its work is recorded. Intended for a Claude Code Stop hook — a turn boundary the watcher can only otherwise infer.

It only ever requests. Automatic checkpoints happen regardless and nothing here can suppress them. Outside a sandbox it exits 0 and does nothing, because not being in one is a normal condition rather than an error.

Automatic checkpoints

During a run a watcher outside the sandbox snapshots the upper on its own. The property is precisely "the agent cannot prevent a checkpoint" — deliberately not "only the harness can request one," which is unbuildable: the agent shares a user and a process tree with whatever launched it, so any proof of identity the harness could present, the agent could replay.

LabelFires whenWhat it caught
[ckpt:idle]writing stops for 2sa resting point — the agent had finished
[ckpt:timed]the ceiling elapses mid-workmid-operation: every file whole, the set of them a moment
[ckpt:hint]something inside askeda turn boundary

Taking one freezes the whole process tree — the sandbox child and every descendant — for the duration of the walk, so no file is caught half-written. A busy agent typically produces none at default settings: idle needs a pause and the ceiling is 90 seconds. The cost arrives with the checkpoints, and checkpoints arrive at rest.

What escapes the layer

Three things are outside the overlay by design, and none of them are captured by a snapshot or reverted by restore:

  • --bind paths. Read-write binds write straight through to the host. --ro-bind doesn't.
  • Your home, in full mode. The default binds $HOME read-write, so an agent can damage files there. full-ro closes this and is the recommended setting for unattended runs.
  • Anything sent over the network. There's no network namespace yet — see below.

Threat model

agentfs is built for "the agent might make mistakes, or follow bad instructions, while editing my code." It is not a VM and doesn't claim isolation against an agent deliberately escaping through a kernel exploit.

Can the agent…full (default)full-ro
modify your project source directlynono
read SSH / cloud credentialsno — maskedno
write to /usr, /etc, system pathsnono
read other users' homes, off-tree pathsno — not visibleno
escalate via sudo insideno — fake root, namespace-scopedno
damage files in $HOMEyesno — read-only
reach the internet, localhost, your LANyesyes
The network is shared. There's no network namespace yet, so the agent reaches everything you can: the public internet, services on localhost, and hosts on your LAN or VPN. Filesystem isolation is what's on offer today. If that matters for your use, run agentfs inside a VM or a network-isolated container until this lands.