chrono-fs: ChronoFS: The Past Is Just Another Directory

A userspace filesystem in C where cat .snapshots/2min-ago/notes.txt works, and where keeping a year of history costs almost nothing.

9 min read • View on GitHub • More from ChirayuSahu

A wide editorial illustration of a giant clock face lying flat like an architectural floor plan, with small directory trees standing upright at four different hours and a single sheet of paper resting at three hours at once, connected by a taut thread. It visualizes the core idea of ChronoFS: time as navigable space and one object existing at many moments.
Time as a place you can walk through. In ChronoFS, every hour on this dial is a directory you can open.
Key Takeaways

The Past Has a Path

The pitch for ChronoFS fits in two shell commands.

cat mnt/.snapshots/@19:53:12/notes.txt
cp -r mnt/.snapshots/before-disaster/src mnt/

Neither of those paths exists on disk. Nothing on your machine created them. mnt/.snapshots/ is a read-only virtual tree that a FUSE daemon synthesizes on demand, and every directory inside it is named after a moment in time. @19:53:12 is not a folder someone made. It is a clock reading, and ChronoFS treats it as one.

The WHEN segment accepts four forms. A relative offset like 2min-ago. An absolute clock time like @19:53:12. A raw epoch number. Or a label you assigned yourself, like before-disaster. Type any of them and you are standing at that instant, reading the tree as it was.

That is the reframe. ChronoFS is not a backup tool you remember to run. It is a filesystem that quietly kept every version and never told you.

Two Data Structures Do All the Work

Two structures carry the whole design, and both live in one header file. include/chronofs.h is the de facto architecture document: it defines the shared types, the on-disk format, and every subsystem prototype. Read it and you have read the project.

The first is an append-only journal. journal.log is a flat array of struct jrec records, each exactly 1,096 bytes. A record holds a timestamp in nanoseconds, a mode, a size, a 32-byte manifest hash, and a 1,024-byte path. There is no padding, because every field is naturally aligned, and journal.c pins the size with a static assertion. It is a path with a little metadata stapled to it.

<store>/current/              live files; the mount passes through to here
<store>/objects/ab/cd...      4 KiB data blocks, named by SHA-256
<store>/manifests/ab/cd...    one manifest per file version (size + block list)
<store>/journal.log           append-only array of struct jrec (1096 bytes each)
<store>/tags                  "name ts_ns" lines
<store>/lock                  flock()ed by the mount daemon
<store>/mounted               mountpoint path while mounted

The second is the content-addressed block store. Every 4 KiB block of file data is written to objects/ under a filename that is its own SHA-256. Every file version is a manifest in manifests/, listing the size and the ordered block hashes that compose it. A snapshot is not a copy of your data. It is a manifest lookup.

Drag the timeline to watch ChronoFS resolve a time expression: parse, resolve, scan the journal backward, then read the winning manifest. The hatched blocks are reused, which is where dedup becomes visible.

Why a Snapshot Costs Almost Nothing

A close-up editorial illustration of three open card-catalog drawers pulled out at different depths, each labeled with a time in hand-lettered script, and each holding a slip of paper bearing the identical hash string. A taut thread runs from slip to slip, and a ledger page beside the drawers shows a column of hex characters. It explains how content addressing lets one block serve many versions.
Three times, one slip. When the content is identical, the hash is identical, and the store keeps a single copy.

Here is the part that makes the trick affordable. Because a block is named by its content, writing a byte-identical file twice stores one block, not two. The second write produces a new journal record and a new manifest, but the manifest points at blocks that already exist.

That is deduplication falling out of the addressing scheme for free. There is no delta encoding, no chunking heuristic, no rolling checksum. Keep four hundred versions of a source tree and you pay only for the bytes that actually changed between them.

Compare the alternatives. Btrfs and ZFS deduplicate at the extent level inside a volume and require you to name a snapshot. restic and borg deduplicate well, but they make you run a command and think in snapshot IDs. ChronoFS deduplicates because of how it names things, and it does it while you work.

Time Is the Query Language

Resolving a WHEN expression is a single function. resolve_when turns 2min-ago, @19:53:12, an epoch, or a tag into a nanosecond timestamp. Then it walks the journal backward and returns the newest record at or before that instant. The scan is linear, the records are fixed-width, and the answer is a manifest hash.

The subcommands wrap that lookup.

gc.c is the unglamorous half. It implements a retention policy with mark-and-sweep: reachable manifests and blocks are marked, everything else is swept. Without it, an append-only store grows forever. With it, history has a budget.

What It Is Not

If you were hoping ChronoFS replaces your version control or your backup system, here is the honest answer. It does neither. It is a local observation layer that sits between raw kernel notifications and a full storage system.

GranularityWho triggers itStorage modelHow you address a version
ChronoFSFile close / fsyncThe filesystem, automaticallySHA-256 content-addressed blocks + manifestsTime expression as a path
GitCommitYou, explicitlyPacked objectsCommit hash or ref
Btrfs / ZFSVolume or subvolumeYou, or a scheduleCOW extents, filesystem-internalSnapshot name
restic / borgBackup runYou, or a scheduleChunked, deduped, remote-capableSnapshot ID + path
WatchmanEventThe filesystem, continuouslyIn-memory query indexQuery, not retrieval
inotify-toolsEventThe filesystemNone (you pipe it somewhere)N/A

Read the table by two columns. Only ChronoFS addresses a version by time and triggers itself automatically. Watchman is automatic but keeps no retrievable history. Git keeps history but waits for you to ask. That gap is the niche.

The Honest Limits

The repository tree shows no tests/ directory, and no test target is visible. That matters. A filesystem that appends to a journal has to answer hard questions about crash consistency, and this one does not document the answers.

None of that is fatal for what this is. ChronoFS is genuinely good for three things: teaching how a versioned filesystem works, experimenting with content addressing, and running on a personal machine where undo is worth more than throughput.