How UNF* works

UNF* watches your filesystem and records every text file change in real time — then lets you rewind any file to any second. This page explains how.

Design choices

UNF* is a background daemon. It runs continuously, so it needs to be invisible: <1% CPU, <100MB RAM, no GC pauses. Rust, no runtime.

Core logic is pure functions with side effects isolated at the edges.

Interface

Same verbs as git (log, diff, restore), applied to time instead of commits.

$ unf watch # start recording $ unf status # what's being watched $ unf log [file] # timeline of changes $ unf log --sessions # detect work sessions $ unf log --global # cross-project timeline $ unf diff --at 5m # what changed in last 5 min $ unf diff --session # what changed this session $ unf restore --at 5m # rewind 5 minutes $ unf restore --at 5m file.rs # rewind one file $ unf restore --session # rewind to session start $ unf cat file.rs --at 1h # view file from 1 hour ago $ unf recap --json # reconstruct context (for agents)

Every mutating command supports --dry-run. Restore creates a safety snapshot before overwriting anything, so restoring is itself undoable.

Use cases

10 real-world scenarios with commands: see the use case carousel on the homepage.

Architecture

Single global daemon, multi-project. One sentinel process supervises the daemon. All storage lives in ~/.unfudged/. Nothing is written to your project directory, so there's nothing to .gitignore.

OS filesystem events (FSEvents / inotify)
Global Daemon (one process, all projects) route_event() → prefix match to owning project Per project: Filter → Debounce → Snapshot → Engine
SQLite (metadata) WAL mode keyset pagination
CAS (file blobs) BLAKE3 addressed 2-level directory
Supervised by: Sentinel (15s health check tick)

Event pipeline

What happens when you save a file:

  1. OS notification. The notify crate (v8) receives the filesystem event. FSEvents on macOS, inotify on Linux.
  2. Route. route_event() finds which project owns the file by longest-prefix match against registered roots.
  3. Filter. 5-layer chain, checked in order. If any layer rejects, the event is dropped:
    • Directory exclusions: hardcoded .git, node_modules, target, etc.
    • Extension exclusions: case-insensitive .png, .exe, .sqlite, etc.
    • .gitignore: parsed and matched if present
    • Hidden files: skipped except .env and .gitignore
    • Magic numbers: first 16 bytes checked for binary signatures (PNG, JPEG, ELF, Mach-O, etc.)
  4. Debounce. Events accumulate for 3 seconds of silence, coalescing multiple saves into one snapshot. On shutdown, force_drain() flushes all pending events. Nothing lost.
  5. Snapshot. Read the file, compute BLAKE3 hash, check CAS for dedup, compute line diff against previous version, write blob + metadata.

Content-addressable storage

Every file version is stored by its BLAKE3 hash, a 256-bit (64-character hex) digest. BLAKE3 was designed for high throughput (~1 GB/s per core), but for text files the speed is academic. We chose it for its simplicity and lack of known weaknesses.

Layout

Two-level directory: the first 2 hex characters become the directory prefix, the remaining 62 become the filename. Two hex digits give 256 possible prefixes (00 through ff), so no single directory holds more than 256 entries. readdir() stays fast regardless of total object count.

~/.unfudged/projects/<hash>/objects/ ab/ cdef0123456789abcdef0123456789abcdef0123456789abcdef012345678901 cd/ ef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd

Deduplication

Before writing, we check if the object file already exists. If two files have identical contents (or a file is saved without changes), we store it once. Simple existence check — no reference counting.

Garbage collection

unf prune deletes snapshot rows from SQLite, then walks the CAS directory and deletes any blob not referenced by remaining rows. Full scan, not incremental. Simple and correct.

SQLite metadata

One database per project, running in WAL mode (Write-Ahead Logging, allows reads while writes are in progress). All timestamps are RFC 3339, always UTC.

Schema

ColumnTypePurpose
idINTEGER PKAuto-incrementing snapshot ID
file_pathTEXTRelative path within project
content_hashTEXTBLAKE3 hex → CAS lookup key
size_bytesINTEGERFile size at capture time
timestampTEXTRFC 3339 UTC timestamp
event_typeTEXTcreate, modify, or delete
line_countINTEGERTotal lines in this version
lines_addedINTEGERLines added vs. previous
lines_removedINTEGERLines removed vs. previous

Descending-timestamp indexes on both per-file and project-wide queries. Cursor-based pagination, so performance stays constant regardless of history depth.

Daemon supervision

Two-process model. A sentinel checks every 15 seconds whether the daemon is alive, and restarts it if it crashed. The daemon runs the watcher, manages per-project state, and writes to storage.

IPC is Unix signals. When you run unf watch, the CLI writes desired state to an intent file and signals the daemon to reload. The sentinel reconciles intent against reality on every tick. If the daemon crashes, it comes back knowing what to watch.

Storage layout

~/.unfudged/ daemon.pid # daemon process ID sentinel.pid # sentinel process ID registry.json # what the daemon is watching intent.json # what should be watched projects/ <project-hash>/ # derived from absolute path metadata.db # SQLite database objects/ # content-addressed blobs ab/ cdef01... # 62-char filename

Each project is isolated by a hash of its absolute (canonical) path. This avoids collisions and keeps projects independent.

Desktop app

The CLI is the engine. The desktop app is a separate binary (Tauri v2 + Svelte 5) that reads ~/.unfudged/ directly. The daemon doesn't know the app exists, and the app doesn't need the daemon running to browse history.

Cross-project timeline with tabbed navigation, activity histogram with drag-to-zoom, word-level diff highlighting, and glob-based file filtering. Live-polls while open, pauses when you switch tabs.

What we don't do (yet)

Honest gaps. These are designed but not built.

← Back to unfudged.dev