flockfs

Mount

flockfs mount ~/FlockfsLive                 # runs in the foreground; Ctrl-C to stop
flockfs mount research ~/ResearchLive    # a specific drive (id, slug or slug-id)
flockfs unmount ~/FlockfsLive               # from another terminal (the mount process then exits)

flockfs mount DRIVE DIR is the same as flockfs --drive DRIVE mount DIR; with one argument it is the folder, and the drive is --drive / FLOCKFS_DRIVE, else the agent token's drive, else the server's default (giving two different drives is an error). The drive is resolved to its id when the mount starts, so renaming it meanwhile changes nothing.

Global options apply as usual (--url, --token/--token-file, --actor, --drive; FLOCKFS_SANDBOX targets a sandbox). Flags: --read-only, --port N (default: any free port), --no-mount (serve only and print the mount command).

DIR must be empty (it is created if missing). Every program sees the workspace there: ls, editors, grep -r, coding agents. Nothing is copied to disk. Reads come from the server; writes go to the server about 300 ms after the last write to a file.

Compared with flockfs sync: sync keeps a real copy (works offline, any tool, full local speed); mount keeps none (nothing to reconcile, always current, needs the server).

How it works

flockfs mount runs a user-space NFSv3 server on 127.0.0.1 (the nfsserve crate) and mounts it with the operating system's own NFS client, so no kernel extension or macFUSE is needed (the FUSE-T / XetHub approach). macOS lets a user mount NFS onto a folder they own without sudo.

macOS command (printed by --no-mount):

/sbin/mount_nfs -o vers=3,tcp,port=P,mountport=P,rsize=1048576,wsize=1048576,acregmin=0,acregmax=0,acdirmin=1,acdirmax=1,soft,sync,nolocks,locallocks,intr,nonegnamecache,deadtimeout=30,nobrowse[,ro] 127.0.0.1:/ DIR
  • acregmin=0,acregmax=0: the kernel asks for a file's attributes before using cached pages (answered from memory), so it never works from content older than what flockfs last told it, and remote changes to files are visible immediately. This costs some speed on read-heavy scans (see below).
  • acdirmin=1,acdirmax=1: folder attributes are cached for a second. Without it (noac) every path lookup asked again for each folder on the way, so each stat/open of pkg/node_modules/x/y.js cost several round trips before reaching the local disk (a plain stat measured 6.6 ms under load, 0.27 ms with this). A listing shows a file created elsewhere up to a second later; looking it up by name finds it at once (nonegnamecache).
  • sync: every write(2) is an RPC, so a refused write (lock, read-only) fails the call that made it rather than a later close.
  • locallocks: flock/fcntl locks work, but only on this machine.
  • soft,intr,deadtimeout=30: if the flockfs mount process dies, calls fail instead of hanging, and the kernel drops the mount after 30 s.

Linux (mounting needs root and an NFS client such as nfs-common, so flockfs mount prints the command instead of running it unless it runs as root):

flockfs mount /mnt/flockfs --port 11111 &     # prints the command below and keeps serving
sudo mount -t nfs -o vers=3,tcp,port=11111,mountport=11111,rsize=1048576,wsize=1048576,acregmin=0,acregmax=0,acdirmin=1,acdirmax=1,soft,sync,mountproto=tcp,nolock,lookupcache=positive 127.0.0.1:/ /mnt/flockfs

A kernel mount smoke test on Ubuntu (Linux 6.8) verified text reads and writes, appends, atomic saves preserving file identity, binary roundtrips, remote file visibility, directory creation, moves, deletion and unmounting. Automatic process exit after an external unmount was not verified because the test server ran in a separate container mount namespace.

Mapping

File systemflockfs
fileidderived from the flockfs file id (root = 1): stable across renames and moves (files created through the mount get a session-local id until the next mount)
lookup, readdir, getattrthe server listing, held in memory. The long-poll change feed refreshes it on every event (30 s safety refresh), so changes made elsewhere appear within tens of milliseconds
size, mtimesize of the current revision; mtime is the server's time of that revision (modified_ms in listings: when the file's content, mode or path last changed). The same in every mount, so cargo/make see an unchanged file as unchanged after a remount; a new revision gets a new mtime, so editors and the kernel notice. Files written through this mount show the time of the local write until the next mount
readwhole file fetched once per revision (GET /files/{id}), ranges served from memory; reads past offset 0 within a second reuse the same snapshot, so a file is never read torn between revisions
write, setattr(size)per-file buffer of offset writes / truncation; committed ~300 ms after the last write, on unmount, and on Ctrl-C/SIGTERM
commitpure appends → POST /files/{id}/append (atomic; concurrent appenders all land). Otherwise PUT /files/{id} with the base revision and the base content (base), which the server three-way merges for text; a stale binary is a conflict
createa new file is uploaded at its first commit (POST /entries, parents created; created with an executable mode, it is uploaded executable)
mode0755 for executable files (and folders), 0644 otherwise; chmod sets or clears the file's executable bit on the server (PATCH /files/{id}). Other mode bits are not stored
mkdir / remove / rmdir / renamecreate folder / DELETE / DELETE / PATCH (move; links are rewritten by the server)
symlinksonly ignored folders are symlinks (see below); creating one is not supported (ENOTSUP)

Concurrent edits

NFS writes are offsets into what the kernel last saw, which may be older than the server's latest content (someone is typing in the browser). The mount remembers what it last served or accepted for each file (its view). A program that rewrites the file (truncate + write, or write-temp + rename) has its change from that view replayed onto the newest content with the server's merge (line level, then character level), and the result is sent with the newest revision as base. Appends whose offset is the end of an older version are moved to the real end. In testing, a simulated browser typing 20 characters/s into line 1 while the mount appended lines and rewrote lines 2 and 3 lost nothing on either side. If the two changes touch the same characters, the mount's version is saved next to the file as name (conflict <unix time>).ext and the file shows the server's version. A stale binary write is handled the same way.

Atomic saves keep file identity

  • Write temp + rename over (VS Code atomic save, os.replace, many tools): the temp file's content becomes a new revision of the existing file (same id, history, links, locks). If the temp file was already uploaded it is deleted.
  • Rename away + new file (vim backupcopy=no, JetBrains ___jb_old___): a file renamed onto a scratch name stays bound to its flockfs file for 3 s; a new file created (or renamed) at the old path within that time is the old file. If nothing takes its place, the rename is carried out on the server after 3 s.
  • In-place saves (>, vim's default here) are ordinary writes.

Local-only names

The sync daemon's litter list (.DS_Store, ._* AppleDouble, .Spotlight-V100, .Trashes, swap/backup files such as *.swp, *~, 4913, .#*, JetBrains temp files, .git, .flockfs) plus .nfs* (the NFS client's "silly rename" of a deleted-but-open file) are accepted and kept in memory only: they work for the programs that create them and are never uploaded. They disappear when the folder is unmounted, including anything under .git.

Ignored paths stay on this machine

The drive's .gitignore and .flockfsignore files (root and nested, gitignore syntax, read from the drive and re-read whenever one changes) decide what the mount keeps local. Nothing created at an ignored path reaches the server.

Ignored folders are symlinks to real local folders. mkdir target (or node_modules, at any depth) where the folder is ignored creates a real folder on this machine and shows it in the mount as a symlink to it:

web/node_modules -> $TMPDIR/flockfs-scratch/<drive id>/live/web/node_modules

The kernel follows the symlink itself, so everything below it (a cargo build, npm ci, thousands of small files) runs on the local disk at native speed; the mount only answers the lookups of the folders on the way (one round trip each, cached for a second). The scratch folder mirrors the drive's layout, so packages that find each other by walking up from their real path still do. Details:

  • Persistent local caches. Mounts use <base>/<drive id>/live, so ignored build folders survive remounts. A concurrent mount uses a private scratch folder if that slot is locked. <base> is FLOCKFS_SCRATCH_DIR, else $TMPDIR/flockfs-scratch. Plain flockfs run uses a private scratch folder.
  • Removal is instant. rm -rf node_modules (and npm ci, which starts with it) removes the symlink; its folder is renamed into <base>/.trash at once and deleted in the background, so the next mkdir starts empty.
  • Renames. A folder renamed onto an ignored path becomes local: cargo creates target/ as .tmpXXXX/ + rename, so the .tmpXXXX folder briefly exists in the drive and is deleted from it by the rename, its files moved into the local folder. An ignored folder can be renamed to another ignored path (its local folder moves too), not to a tracked one (EXDEV). Moving a tracked folder moves the ignored folders inside it along.
  • Ignore files changing. When a path stops being ignored its symlink disappears (the local folder is kept and comes back if the rule does); when a path becomes ignored, entries already in the drive there are hidden and a new mkdir makes a local folder. Rules apply to names as they are created, from the ignore files in the drive (an edit is in effect once saved, ~0.3 s).
  • Cleanup. When a mount starts it removes, in the background, the trash, private folders of mounts that are gone, and stable folders of its drive that were unused for 14 days. Folders in use are never touched. To reclaim space by hand: rm -rf $TMPDIR/flockfs-scratch while nothing is mounted.

Ignored files outside an ignored folder (.env, *.sqlite) are listed, readable, writable and chmod-able like any other, are never uploaded, and are kept by the mount process (large ones in a temporary file) until it ends. Entries already in the drive at ignored paths are not shown. A scratch file renamed to a non-ignored path is uploaded then; a file that becomes ignored after it was uploaded stays in the drive but is not shown any more.

Why symlinks: the alternative, serving ignored folders from the mount process (what this did before), sends every open/write/unlink of a build through NFS RPCs to a user-space server. npm ci of 14 packages (3,085 files; ~1 s on a local disk) did not finish in 10 minutes and could not be interrupted (processes stuck in uninterruptible NFS waits), removing what it left took 220 s, cargo's incremental cache failed on its lock file (ENOTSUP) and big builds hit ESTALE. A symlink costs one lookup per path and keeps no build state in the server.

Locks

  • Someone else's whole-file lock: the first write (or O_TRUNC) fails immediately with EACCES ("Permission denied"); nothing is buffered. The mount log names the holder, the reason and the expiry.
  • A section lock is only known when the commit is refused (423): the buffered version is kept (and readable in the folder), further writes to that file fail with EACCES, the log names the holder, and the commit is retried every 5 s, landing once the lock is released. If the folder is unmounted first, the unsaved content is written to $TMPDIR/flockfs-mount-unsaved-<time>/<path> and the path is printed.

Read-only

A read-only credential (role: viewer, e.g. a read-only agent token) or --read-only mounts with ro; every change fails with EROFS ("Read-only file system").

Stopping

Ctrl-C, SIGTERM or flockfs unmount DIR (which tries umount, then diskutil unmount force, then umount -f). The mount process unmounts first, while it is still serving, so the kernel flushes its last writes; then it commits every buffered write and exits. Plain umount DIR works too: the process notices within a second.

Measurements

MacBook (Apple silicon), macOS 26, release builds, local server and PostgreSQL:

mount~20 ms
stat0.13 ms
cat of a 1 KB file0.4 ms
ls of 10 folders (300 files)3.5 ms
grep -r over 300 files (1 KB each)430 ms cold, 180 ms warm (with actimeo=1 instead of noac: 16 ms warm, but see noac above)
append (>>) syscall~4 ms; on the server after ~400 ms (300 ms idle + tick)
overwrite → on the server~400 ms
API write → visible in the folder~10 ms
cp of a 3 MB binary37 ms; on the server after ~0.5 s
mkdir / rm / mv6 ms / 28 ms / 65–100 ms (the server's link rewriting dominates mv)

Ignored folders (October 2026, debug builds, a machine at load average 25–110 from parallel builds, so absolute numbers are noisy; before = the per-mount in-memory scratch):

beforeafter
npm ci in packages/bash-tool (14 packages, 3,085 files; 0.9–1.1 s natively)> 10 min, unkillable (killed with -9)9–12 s; a second npm ci 32–35 s
rm -rf node_modules (2,192 entries)220 s0.05–0.08 s
3,000 small files below node_modules/ (test)—0.74 s
cargo build of a 20-module crate, target/ in the mountfails (ENOTSUP on the incremental lock); with CARGO_INCREMENTAL=0 9.4 s, and a target folder lands in the drive1.3 s
second cargo build in a fresh mount of the same driverecompiles (mtimes = mount start): 0.7–1.2 s, Compiling; with target/ in the mount it is gone anyway0.15–0.18 s, fresh

Limitations

  • Needs the server: no offline mode. With the server gone, calls fail after the soft timeout and unsaved buffers are kept in memory until the process exits (then dumped as above).
  • Writes are durable on the server ~300 ms after the last write, not when write(2) or close return (NFSv3 has no close). fsync does not wait for the commit.
  • Text files are fetched whole per revision, binary files are base64 over JSON: large files (up to the 25 MiB limit) are slow to read the first time and to commit.
  • No symlinks (other than ignored folders), no hard links, no permissions other than the executable bit, no extended attributes (macOS stores xattrs in ._* files, which stay local). Times set by touch -t are ignored. Paths through an ignored folder still pay one NFS lookup per folder on the way (npm ci measured 9–35 s vs ~1 s natively on a heavily loaded machine); a build that needs full speed can use the real folder (cd "$(readlink target)" or CARGO_TARGET_DIR).
  • File handles do not survive a restart of flockfs mount (the kernel reports stale handles; remount).
  • Local-only files (including .git/) live in memory and vanish on unmount, as do ignored files outside ignored folders; ignored folders persist per drive (see above).
  • Linux mounting is documented but not tested; Windows is not supported.

flockfs run

flockfs run [--drive D] [--path FOLDER] [--read-only] [--agent-token-name NAME [--scope FOLDER]]
         [--sync | --mount] [--dir DIR] [--keep] [-q] -- COMMAND [ARGS…]

flockfs run -- claude
flockfs run --drive research --path inbox -- python agent.py
flockfs run --read-only -- grep -r TODO .

Runs COMMAND in a live workspace (the idea of Turso AgentFS's agentfs run):

  1. Workspace. The drive is mounted as above (same NFS server, in the flockfs run process) at a new /tmp/flockfs-run-XXXXXXXX folder (0700), or at --dir DIR (must be empty for a mount). --read-only (or a viewer credential) mounts ro.

  2. Fallback. If the mount cannot be made (Linux without root, mount_nfs failing, FLOCKFS_RUN_NO_MOUNT=1), flockfs run says so and uses a synced folder instead: one full flockfs sync --once pass, then the sync daemon in its own process group for live updates both ways; at the end the daemon is stopped with SIGTERM and a last pass sends what is left. The folder then contains .flockfs/ (checkpoint, run-sync.log). With --read-only the synced folder is a read-only snapshot taken at start (files and folders chmod a-w, no live updates, nothing sent). --sync always uses this mode; --mount fails instead of falling back. The mount is the default because it needs no copy and starts in milliseconds; the fallback keeps flockfs run working everywhere the sync daemon does.

  3. Command. It starts in the folder (or --path FOLDER inside it, created if missing and writable) with stdin/stdout/stderr passed through and this environment:

    VariableValue
    FLOCKFS_URLthe server
    FLOCKFS_DRIVEthe drive's permanent id
    FLOCKFS_TOKEN_FILEa 0600 file in $TMPDIR holding the run's token, deleted afterwards (FLOCKFS_TOKEN is removed from the environment; the CLI reads FLOCKFS_TOKEN_FILE)
    FLOCKFS_WORKSPACEthe mount (or synced) folder
    FLOCKFS_SESSIONa new UUID; every flockfs Client with it set (the CLI, and flockfs run's own mount or sync) sends it as X-Flockfs-Session, so the run's changes can be grouped

    A short header names the drive and folder on stderr unless -q.

  4. Signals. SIGINT, SIGTERM, SIGHUP and SIGQUIT are caught by flockfs run and passed to COMMAND; a Ctrl-C typed at the terminal already reaches COMMAND (same process group), so it is not sent twice. A signal never skips cleanup.

  5. End. When COMMAND exits: unmount (the kernel flushes its last writes), commit every buffered write (up to 30 s; what cannot be saved, e.g. refused by a lock or permissions, is copied to $TMPDIR/flockfs-run-unsaved-<time>/ and reported), remove the folder unless --keep (a --dir folder is only removed if flockfs run created it and it is empty; a folder that is still mounted is never emptied), delete the token file, and exit with COMMAND's exit code, or 128 + N if it was killed by signal N. Errors of flockfs run itself exit 1.

Least privilege: --agent-token-name NAME [--scope FOLDER]. For drive owners: before COMMAND starts, flockfs run creates an agent token labelled NAME (POST /api/tokens, days: 1) with write (read with --read-only), limited to --scope FOLDER (default: --path; with neither, the whole drive). Both the workspace and COMMAND use that token, so COMMAND sees only what the token may read, cannot change anything outside the scope (through the folder or the API), and its changes are recorded as agent:<token id> and shown as NAME. When COMMAND exits the token is revoked (DELETE /api/tokens/{id}); if that fails it is reported, and the token expires within a day anyway. Through a mount, creating a file where the token may not write succeeds locally but is refused when committed (the copy is saved as described in step 5). Non-owners get an error.

On this page