Daemon & FUSE
softfig-keeperd is the per-device daemon. It is the one process that holds the
unlocked vault, the one writer to the store, and the one thing that turns your edits
into signed commits. This page explains its three moving parts: the watcher
pipeline that classifies changes, the FUSE view that shows you plaintext without
ever writing plaintext, and the lock/unlock lifecycle that governs when any of
it is possible.
For the workspace-wide picture see Architecture; for the committed data structures see Version Control.
Born in FUSE
Section titled “Born in FUSE”The defining trick: plaintext never lands on disk. The canonical state under the
state root is all ciphertext — encrypted blobs, a SQLite index, wrapped keys. On
unlock, the daemon mounts a FUSE
filesystem at your garden root and projects the decrypted content there. When you
cat a file, the bytes are decrypted in the daemon’s memory at read time and handed
to the kernel; they exist as plaintext only in RAM, only for that read. On lock, the
mount goes away and the garden root is empty again.
This is why a freshly onboarded garden is “born in FUSE”: softfig onboard writes
the first commit straight into the encrypted state-root layout and never materializes
a plaintext tree to convert. There is no plaintext copy to leak, back up by accident,
or forget to clean up.
The watcher pipeline
Section titled “The watcher pipeline”Writes don’t always come through a verb — you can edit a file in the mount with any editor. The watcher is what notices, and it feeds the same commit path the typed verbs use.
inotify ─▶ notify-debouncer-full ─▶ DirtyEvent ─▶ accumulator ─▶ classify ─▶ commit (200 ms debounce) │FUSE write ─▶ AccumulatorSink ─────────────────────┘ (same accumulator)- Two sources, one accumulator. A source-agnostic
DirtySetAccumulatorcollects change events. AnInotifyDriver(built on thenotify+notify-debouncer-fullcrates) feeds it filesystem events with a 200 ms debounce; FUSE writes feed the same accumulator through anAccumulatorSinkadapter. So a change made by an editor and a change made through the mount reach one identical classifier — a single pipeline, not two. - Self-write suppression. When the daemon itself writes (a verb commit, a reveal temp file), it registers those paths in a suppress window (~500 ms) so its own I/O doesn’t bounce back as a spurious “manual edit.”
- The intent classifier. Before committing, the daemon labels the change. The
classifier only assigns a specific intent when the dirty set is homogeneous and
matches a rule — a single new
journal/decisions/decision-<slug>.md→decision_logged, a newincident-…→incident_logged, renames intojournal/archive/**→archive_move— and otherwise falls back tomanual_edit. Typed verbs skip the guessing: each carries its own intent (note_added,config_migrated, …). Intents are a closed enum; see Version Control → Intent.
The FUSE view in detail
Section titled “The FUSE view in detail”The mount is more than “decrypt on read” — it also carries Layer B’s redaction and an in-memory overlay for writes.
Reads. For a normal file the daemon looks it up in the committed tip, decrypts the
Layer-A blob with the in-RAM VaultSession, and returns the bytes. Two Layer-B cases
intercept that:
- A whole-file sealed path (matched by a glob in
sealed-paths.toml) never decrypts through the mount. It returns the placeholder[sealed:<path>]instead, computed on the fly, never stored. - A file containing inline
<vault id="…">regions decrypts its Layer-A body, then has each sealed region replaced with[encrypted]before the bytes leave the daemon.
Because the redaction happens inside the daemon, a sealed secret is invisible to
anything reading through the mount — including a Claude session. The only way to see
the plaintext is the CLI-and-human-only softfig reveal, covered in the
Secrets guide. The read-only IPC browse verbs
(list_tree / read_file) apply this exact same redaction, so the TUI and MCP see
placeholders too.
Writes. A write through the mount does not commit immediately. It lands in an in-memory overlay (a write buffer with a generation counter); the overlay fires a dirty event into the accumulator; and the daemon’s flush later folds the overlay into one commit. When a new tip is published, the overlay and the read caches are invalidated. Building the commit reads from the daemon’s in-memory snapshot rather than by walking the mounted directory — otherwise the commit walk would read through its own FUSE mount and deadlock.
Lock and unlock
Section titled “Lock and unlock”The daemon boots Locked. In that state only two IPC verbs answer — status and
unlock; everything else is refused until a session exists.
- Unlock takes the passphrase, runs it through the vault to produce a
VaultSession(the decryption keys, held in RAM), opens the repo, and — for a FUSE-configured garden — mounts the plaintext view. The session is shared (behind anArc) with the FUSE layer so reads can decrypt without contending on the daemon’s main lock. The key derivation is entirely inside the vault. - Lock / shutdown drops the session, repo, and mount. Dropping the mount handle unmounts it; dropping the session zeroizes the keys. The garden root is plaintext-free again.
Relock (daemon cycle). An unattended work loop has to survive a daemon restart
— you rebuilt the binary, say — without a human retyping the passphrase. softfig daemon cycle does this with a one-time relock token: the outgoing daemon wraps
its live key under a fresh random token and writes the blob to $XDG_RUNTIME_DIR
(tmpfs, so a reboot wipes it); the incoming daemon redeems it to re-establish the
session. The token is single-use, expires after 20 minutes (bound into the wrap’s
associated data), and is scoped to that exact vault by fingerprint.
This is a deliberate weakening, so it is opt-in and off by default
(allow_relock in the plaintext bootstrap keeper.toml, set by hand). Anyone who can read the token
while it is live can unlock the way the loop does. softfig vault rewrap invalidates
every outstanding token. The mechanism lives in the vault; the
Vault page covers the key math.
The IPC surface
Section titled “The IPC surface”Frontends talk to the daemon over a JSON-Lines protocol on a Unix domain socket at
$XDG_RUNTIME_DIR/softfig-keeperd.sock. Each request is one line —
{"v":1,"op":"<verb>","args":{…}} — and each reply is one line, either
{"ok":true,"data":…} or {"ok":false,"error":"…","kind":"…"}. Every accept is
authenticated twice over: the socket is mode 0600, and the daemon checks the peer’s
UID with SO_PEERCRED on each connection.
The verb set spans the whole feature surface — status/unlock, commit/log/show,
the typed document verbs (log_decision, add_note, patch_file, batch, …) and
their surgical-write siblings, the read-only browse verbs, Layer-B vault_*,
pairing pair_*, replica replica_*, shared-subtree lifecycle, deploy, and the
growlight queue verbs. The full catalogue is MCP verbs
(the MCP bridge is a thin translation of it) and the CLI.
growlightd alongside keeperd
Section titled “growlightd alongside keeperd”The autonomous work loop runs in a separate process, softfig-growlightd, a user
unit with its own socket ($XDG_RUNTIME_DIR/softfig-growlightd.sock). It is a
client of keeperd, never a part of it, so a crashing agent can’t take the vault
session down. keeperd supervises the unit: it starts growlightd on unlock when the
garden’s growlight gate is on, and stops it on a real lock. The
Growlight area covers operating it.
Section titled “The autonomous work loop runs in a separate process, softfig-growlightd, a user
unit with its own socket ($XDG_RUNTIME_DIR/softfig-growlightd.sock). It is a
client of keeperd, never a part of it, so a crashing agent can’t take the vault
session down. keeperd supervises the unit: it starts growlightd on unlock when the
garden’s growlight gate is on, and stops it on a real lock. The
Growlight area covers operating it.”Related: Architecture (the crate map and the write pipeline), Version Control (what a commit contains), The Vault (the keys the session holds), and the Secrets guide (reveal, the only way past Layer B).