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.
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 path ↑ upper 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:
| Kind | Created by | Retention |
|---|---|---|
| manual | snap, with a name you choose | kept until you gc |
auto-<ts> | sandbox exit | kept until you gc |
ckpt-<class>-<ts> | the watcher, during a run | self-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 overlayfs | FUSE | |
|---|---|---|
| speed | at parity with no sandbox | ~2.2× on an incremental build |
| where it mounts | inside the sandbox | host-side |
| needs | kernel ≥ 5.11, and ~/.agentfs not itself on overlayfs | libfuse3 |
| delete marker | character 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.
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:
| mode | Effect |
|---|---|
full | Default. Whole $HOME read-write, with credential paths masked. Dotfiles and tools work with no configuration. |
full-ro | Same view, bound read-only. Blocks the "agent deletes my home" class outright; paths that must stay writable get their own overlay. |
allowlist | No 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.
| Flag | Effect |
|---|---|
--mount-backend | auto (default), kernel, or fuse. |
--snapshots | all (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-binds | Print 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.
| Label | Fires when | What it caught |
|---|---|---|
[ckpt:idle] | writing stops for 2s | a resting point — the agent had finished |
[ckpt:timed] | the ceiling elapses mid-work | mid-operation: every file whole, the set of them a moment |
[ckpt:hint] | something inside asked | a 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:
-
--bindpaths. Read-write binds write straight through to the host.--ro-binddoesn't. -
Your home, in
fullmode. The default binds$HOMEread-write, so an agent can damage files there.full-rocloses 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 directly | no | no |
| read SSH / cloud credentials | no — masked | no |
write to /usr, /etc, system paths | no | no |
| read other users' homes, off-tree paths | no — not visible | no |
escalate via sudo inside | no — fake root, namespace-scoped | no |
damage files in $HOME | yes | no — read-only |
reach the internet, localhost, your LAN | yes | yes |
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.