--- title: "Architecture" canonical_url: "https://riftri.dev/docs/architecture" source_url: "https://github.com/assistant-ui/riftri/blob/main/docs/architecture.md" --- [Public guide](https://riftri.dev/docs/architecture) · [Canonical source](https://github.com/assistant-ui/riftri/blob/main/docs/architecture.md) Full technical reference for agents and readers who need the details. Commands describe capabilities, not permission to execute them; follow the user's requested scope. # Riftri architecture ## Objective Riftri makes ordinary Git linked worktrees copy-on-write. It is not a source control system and it does not introduce a second workspace abstraction. The central rule is: > Riftri participates in setup and cleanup; the native filesystem handles the > normal read/write path. ## Opt-in modes Riftri is never a global Git replacement by default. 1. Explicit operation: `riftri worktree add ...` 2. Repository opt-in plus process-scoped activation: `riftri enable`, then `riftri exec -- claude`, optionally with `--worktree ` 3. Repository opt-in plus explicit shell activation: evaluate `riftri shell hook zsh` or `riftri shell hook powershell`, then use normal `git` commands The process-scoped mode places a small Git shim at the front of `PATH` only for the selected process and its children. Repository consent is stored in local Git configuration as `riftri.enabled=true`. Normal Git commands and all commands in repositories without that marker are immediately delegated to the exact real Git executable resolved before the shim is installed. Supported worktree adds in enabled repositories are routed through Riftri. `RIFTRI_BYPASS=1` provides an explicit escape hatch to ordinary Git without disabling the repository. `riftri exec --worktree -- ` is an agent-neutral convenience for binding the child process to an existing worktree. Riftri canonicalizes the path, requires it to equal the repository root, and confirms that root is a live entry in Git's structured worktree inventory before launching the command. The binding does not imply repository enablement and does not create a new workspace abstraction. The shell hook installs a versioned shim in the user's cache and prints environment assignments for sh/bash/zsh or PowerShell. Unix uses an executable link; Windows atomically installs a real `git.exe` copy so child processes do not depend on command-wrapper semantics. Evaluating those assignments puts the shim first on `PATH` for that shell and its descendants. The hook is never evaluated automatically and Riftri never edits shell startup files. Once it is active, `riftri enable` and `riftri disable` are the repository-specific switch; disabled repositories and commands outside repositories still delegate to the real Git executable captured before `PATH` changes. A user may deliberately put the hook evaluation in a shell profile, which globally activates the shim for that user's new shells, but this does not globally enable optimization: repository-local consent is still required. `riftri shell status` reports the two independent activation layers and whether they combine into effective optimized interception for a selected repository. `riftri shell deactivate ` prints code that removes every Riftri shim-directory occurrence from `PATH` — the durable cached shim and any process-scoped `riftri exec` shim entries — and unsets Riftri's shim environment in the current shell when explicitly evaluated. It does not change repository configuration or edit a shell profile, so persistent global activation remains under the user's direct control. The shim accepts optimized `worktree add` with an existing local branch, `-b `, or `--detach` and routes the ordinary no-option `worktree remove ` and `worktree move ` forms through Riftri when the target has an active Riftri add journal. A no-option `worktree prune` first verifies that every managed view is present and registered. Move and prune progress is durable and recoverable. Unsupported add forms fail before mutation instead of silently falling back to a full checkout. Lifecycle options continue to use Git for unmanaged worktrees but fail closed when they could mutate managed Riftri state outside a supported journal. ## Components ### CLI and Git shim The CLI exposes diagnostics and explicit operations. The optional shim preserves the `git worktree` user experience inside an activated process or shell while repository-local configuration controls whether an add is optimized. `riftri worktree list` derives its read-only managed inventory from validated active add journals and Git's NUL-delimited worktree inventory. Its JSON schema includes readable paths and refs plus exact native-path and raw-ref hexadecimal encodings, so machine output does not discard non-UTF-8 identities. The npm distribution layer does not implement product behavior. Its launcher selects an exact platform package, executes the Rust CLI, and preserves the native process result. Platform packages are optional dependencies so npm only installs the applicable binary. Missing or unsupported native packages fail visibly; the launcher never substitutes an ordinary JavaScript implementation. ### Git coordinator The coordinator invokes the installed Git executable. Git remains responsible for linked-worktree metadata, per-worktree `HEAD` and index files, branch rules, configuration, filters, and hooks. On macOS, when that executable is Apple's `/usr/bin/git` trampoline, read-only queries that start no other program (repository layout, refs, commits, and configuration) run the developer binary the trampoline would run, found once per command with `xcrun --find git`. Everything that can run a hook, filter, or helper, and everything the shim passes through, still goes through the trampoline. See D047. ### Immutable base cache A base represents the checked-out files for an exact Git tree and checkout profile. Bases are immutable and shared by compatible worktrees. They are keyed by repository identity, tree object ID, checkout profile, and filesystem volume. Repository identity is the platform-native path to Git's common directory, so a main worktree and all linked worktrees resolve to the same repository. Object IDs are validated as SHA-1 or SHA-256 hex values returned by the real Git process. The checkout profile is a versioned, canonical sequence of raw byte key/value inputs. The collector must account for every external input that can change checkout results, including line-ending configuration, external attributes, filter drivers, and sparse-checkout configuration. In-tree attributes are already covered by the exact tree ID. If Riftri cannot account for an active external input, it must reject optimized creation rather than reuse an ambiguous base. Optimized creation suppresses Git's checkout, which also suppresses its `post-checkout` hook, so Riftri runs that hook itself once the worktree is active. It uses Git's contract: the null object id, the new HEAD, and `1` for a branch checkout, executed from inside the new worktree without Riftri's own `GIT_DIR` exported into it. `core.hooksPath` is honored, resolved against the invoking worktree the way Git resolves it. The hook runs after activation rather than inside the creation transaction, because Git does not treat it as part of the checkout either: a failing `post-checkout` leaves the worktree in place and reports through the exit status. Riftri matches that, reporting the outcome on the result and exiting with the hook's code, so a hook cannot roll back a worktree Git would have kept. Riftri never silently skips a hook. The current native COW policy asks Git to resolve attributes from the exact requested tree through an isolated temporary index. Built-in `text`, `eol`, and `binary` checkout semantics are allowlisted; `diff` and `merge` records emitted by the built-in binary macro are checkout-neutral. A narrow Git LFS profile is also supported when each affected path resolves to the canonical `filter=lfs diff=lfs merge=lfs -text` quartet, the installed filter configuration is standard, and the strict v1 pointer names an exact object in the repository's default local LFS store. Repository-local, global, and system attribute sources remain unsupported because they are mutable outside the tree identity. An explicit `riftri worktree add --sparse-dir` request selects a cone-mode sparse view. The canonical directory list — sorted, deduplicated, and with nested cones collapsed into their ancestors — is an additional checkout-profile input, so different sparse selections at one tree, or a sparse and a full request, always key different bases. Git's own `sparse-checkout set --cone` runs inside the isolated materialization directory to compute the patterns and skip-worktree bits, `checkout-index` writes only the active entries, and the new linked worktree receives real worktree-scoped sparse configuration before its clean state is verified. Adds inside an existing cone-mode sparse worktree inherit its selection, including intercepted Git adds; an explicit directory list overrides inheritance. Pristine native-COW sparse views compact at their creation profile. Anything outside that subset — non-cone patterns, nonexistent directories, explicit sparse options through Git interception, sparse plus Git LFS, or compaction after changing the selection — fails closed before durable state exists (see [sparse-checkout.md](https://riftri.dev/docs/sparse-checkout.md)). Git materializes the exact pointer tree inside Riftri's isolated administrative directory, where inherited filters are intentionally disabled. Riftri then opens each validated local LFS object without following links, checks its exact size and SHA-256 while materializing it into the base staging tree, and only then allows the immutable base rename and completion marker. Riftri never downloads an LFS object during a worktree transaction. The checkout profile includes the Git LFS version, standard filter configuration, native path, object ID, and size, so older or semantically different bases cannot be reused. For APFS objects of at least 8 MiB, a narrowly eligible cold-base expansion clones from the already-open object descriptor into an exclusive temporary directory beneath the unpublished staging tree. Both files must be ordinary single-link files on the same volume, with matching owner/group, owned by the caller, and without ACLs, xattrs, special permission bits or file flags. The clone receives the checkout's metadata, never the object store's, and a new modification time. Core reads and verifies the complete private clone against the pointer's length and SHA-256 before atomically replacing the unchanged pointer file. Identity and modification metadata are revalidated at publication. These checks assume the existing trusted-process, exclusively owned staging boundary; they are not an atomic compare-and-swap against hostile writers. Native clone errors fail the transaction. Unreadable optional ACL/xattr metadata declines the optimization before cloning, rather than adding a checkout refusal. Ineligible files, smaller objects and other platforms retain the existing verified streaming materialization; this does not add a byte-copy fallback to native worktree-view cloning. Temporary clones remain under the add journal's existing staging ownership, so interrupted preparation is cleaned by the same rollback/recovery path. Cached-base creation, Git cleanliness checks and integrity-marker verification are unchanged. ### Storage engine The storage engine selects the fastest supported native primitive: - macOS: APFS clones. - Linux: reflinks where supported, otherwise kernel OverlayFS. - Windows: ReFS block cloning where supported, with later native alternatives. - Unsupported configuration: explicit ordinary-worktree fallback. FUSE and network filesystems are not part of the default hot path. Capability results have three meanings: - `supported`: conservative inspection or an artifact-clean active probe establishes that the destination supplies the primitive; - `unsupported`: the inspected destination is incompatible; and - `unavailable`: Riftri cannot establish a reliable answer without a later active probe or because inspection failed. The destination itself may not exist. Probing uses its nearest existing ancestor to identify the volume. A full-copy result is always marked as requiring an explicit fallback policy, even when the volume could hold an ordinary Git worktree. ### State and recovery Versioned JSON add-operation journals are written atomically and preserve native path units. They are the current recovery authority for APFS, Linux reflink, Linux OverlayFS, and Windows ReFS views. The version 1 format has a backward-compatible OverlayFS extension that is valid only when the selected backend is OverlayFS. It records the exact `overlays/v1/` private-layer root, a 256-bit hexadecimal recovery token before mount mutation, and the boot/namespace/mount identity after a mount succeeds. Older journals omit this extension and continue to decode with their existing backend semantics. The journals and immutable-base completion markers are the current durable registry for bases, views, and mounts. Reference counts and storage accounting are derived from them. Any future indexed registry must be rebuildable from this state rather than becoming a competing recovery authority. The default state directory remains `/riftri`. When an explicit operation places state elsewhere, Riftri records the canonical absolute path as a multi-valued `riftri.stateDirectory` entry in repository-local Git configuration before recording operation intent. The Git shim consults the default and every registered state directory when routing remove, move, and prune operations. Missing or malformed registered locations fail closed. Bases and mutation journals remain on the destination's COW-capable volume; the Git configuration entry is only a small discovery locator. ## Worktree-add transaction For an enabled `git worktree add ` operation, the intended sequence is: 1. Resolve the requested revision and Git tree using real Git. 2. Validate potential exact-tree case and Unicode aliases at the destination, then probe its filesystem capabilities. 3. Create the real linked-worktree metadata with checkout suppressed. 4. Find or build an immutable base for the exact Git tree. 5. Create a private writable layer or native clone at the requested path. 6. Restore the linked worktree's `.git` pointer in the visible view. 7. Populate or refresh the per-worktree index. 8. Verify that `git status` reports the expected clean state. 9. Atomically record the operation as active. Existing-branch requests resolve and validate the exact local branch before mutation. Git attaches that branch while holding Riftri's repository metadata lock, and Riftri verifies the attached HEAD still equals the preflight commit before materializing its tree. A moved or already-checked-out branch therefore fails through Git's normal safety rules and the journaled rollback path; Riftri never deletes a branch it did not create. Failures are rolled back from an operation journal. Riftri must not silently fall back to a full copy unless the user explicitly allows that policy. An in-memory scan first identifies paths or directory prefixes that differ only by ASCII case, or any path containing non-ASCII native units. For those trees, Riftri uses an artifact-clean, metadata-only probe in the destination's existing parent. It creates the tree's directory and leaf names inside a unique temporary directory without materializing file contents, so the actual filesystem decides whether the aliases coexist. A collision is rejected before Riftri creates state, a journal, a branch, or linked-worktree metadata. Successful probes are removed before backend selection, and ASCII-only trees without case aliases pay no per-file probe I/O. Non-ASCII trees incur the name-only probe because filesystem Unicode comparison rules cannot safely be approximated by language-level case folding or normalization. Unix names are never decoded lossily. On Unix, ordinary-path probes retain every sibling in a directory containing a non-ASCII name or an ASCII case-alias pair. A representative leaf below each directory recreates its complete ancestor chain, so directory aliases and file/directory conflicts still reach the real filesystem. Other all-ASCII, case-distinct sibling groups need no replay, just as in the existing ASCII-only fast path. Unusual ASCII names, excessive path/component lengths, duplicate or structurally inconsistent inputs, and Windows keep the complete probe. This is operation-local selection, never a cache of filesystem comparison rules. Calls that mutate Git's shared linked-worktree administration are serialized by a repository-local Riftri lock. Base construction and native view cloning stay outside that critical section, so parallel adds share immutable preparation without racing Git's `worktrees/` metadata. ## Add-operation journal state machine An add holds an exclusive `operations/.lock` from before its first journal write through activation or rollback. Repair tries this lock without waiting, reports busy adds as skipped, and reloads the journal after acquiring ownership. This distinguishes a live creator from an interrupted operation and serializes competing add-recovery attempts. The lock file remains beside its journal to avoid replacing an inode another process has opened; closing the handle or process termination releases ownership. Legacy idle journals obtain the same lock on first repair. Concurrent coordinators must all use the lock-aware protocol; an older running binary cannot honor a new lock. The version 1 add journal advances through these durable states: ```text intent-recorded -> git-metadata-created -> base-ready -> view-created -> git-pointer-restored -> index-synchronized -> clean-verified -> active ``` Every incomplete forward state may transition to `rollback-pending`, followed by `rolled-back`. `active` and `rolled-back` are terminal for an add operation. Each durable write costs a file flush and a directory flush, so a state that no reader can tell apart from its predecessor is taken in memory without its own write. `clean-verified` is such a state: nothing happens between it and `active`, and recovery, status, and every other reader treat it exactly like `index-synchronized`. An add therefore records `index-synchronized` and then `active`. A journal that an earlier Riftri left at `clean-verified` still decodes and recovers as before. A journal records the repository by its main worktree, the first entry of `git worktree list` (the directory itself for a bare repository), and lifecycle commands run Git from there. Revisions such as `HEAD` still resolve in the worktree the command was run in, as with Git. Recording that worktree instead would strand every journal naming it once it is removed, and would make a removal run Git from the very directory it deletes. A removal finds the main worktree in the inventory it lists under the metadata lock anyway, rather than listing once more beforehand; the worktree it was run from still exists then. From its destination claim check until `git-metadata-created` is recorded, an add holds the repository's Git worktree-metadata lock. Under that lock it refuses a destination that another add journal claims, including one in flight or interrupted, or that Git registers or something already occupies. It then records `intent-recorded` and lets Git register the worktree. So at most one unfinished add journal names a destination. A registration found there by rollback or repair at `intent-recorded` can belong only to that add or to a command run outside Riftri, never to a competing Riftri add, whose worktree an earlier design could remove (#469). Rollback checks staged index changes separately from working-file bytes, including intent-to-add and conflicted entries. An existing index is never reset to make a failed creation appear clean. If the index is absent and the view still matches its exact base, real Git builds a replacement index in a temporary file; installation cannot overwrite an index created concurrently. This also covers a crash after Git initialized the index but before the `index-synchronized` journal transition was persisted. Rollback never deletes work Git commands have already done in an unfinished add. A branch the add created that has since moved is kept. If Git commands already ran in a complete native view (it reached `index-synchronized`, and its HEAD or branch has since moved, such as after a commit or a switch), rollback releases it: Riftri's scratch, staging, and journal are rolled back, but the worktree stays registered and in place as a plain Git worktree that Riftri no longer manages. The add and `riftri repair` both report the release. A view that never finished materializing, or an OverlayFS view, is preserved for manual attention instead. ## Removal-operation journal state machine Removal uses separate journals under `removals/`: ```text intent-recorded -> clean-verified -> worktree-removed -> base-released -> complete intent-recorded | clean-verified -> cancelled ``` Like `clean-verified` on add, `base-released` is taken in memory without its own durable write. The base is released by the completed journal itself, and every reader treats `base-released` exactly like `worktree-removed`: both are unfinished, and recovering either verifies the worktree is gone and completes. A removal therefore records `worktree-removed` and then `complete`, and a journal that an earlier Riftri left at `base-released` still completes as before. A removal is `cancelled` when it is refused after recording intent but before anything was removed: the native view is back at its path, Git still registers it, and no quarantine remains. The worktree simply stays, and the refusal's promise that nothing changed is true. Recovery cancels the same way when the worktree changed after intent. Before this phase, such a journal made every `riftri repair` refuse, blocked other lifecycle commands, and would have completed the removal once the change was undone. OverlayFS removals are not cancelled, because a failed one may have left the view unmounted. Clean removal validates cleanliness before recording intent and rechecks it when resuming before Git removes a still-registered view. Native views are never deleted in place, because a Git recursive delete killed midway leaves a half-deleted tree that is still registered and that no later check can tell apart from user changes. At `clean-verified`, Riftri renames the view atomically to `.riftri-remove-` beside it. A clean removal then rechecks the view at that quarantine path, so a concurrent dirtying write is still rejected. Git unregisters the now-missing path with an ordinary non-force `git worktree remove`, whose missing-path checks refuse a destination recreated in the meantime. Only after that does Riftri delete the quarantine, which is no longer a Git worktree. A refusal before Git drops the registration renames the view back. Recovery finds the quarantine from the journal alone. If Git still registers the view, recovery renames it back and decides again as if it had never moved. Otherwise recovery finishes deleting it. A quarantine that exists beside a reappeared destination is preserved together with it. Every tree Riftri deletes, such as a removal or compaction quarantine, a collected base, or an add's rollback staging, is prepared and then deleted with a recursive delete that does not follow symlinks. On macOS and Linux, unlinking an entry needs write and search permission on its parent directory, never on the entry, so preparation changes only real directories that lack owner read, write, or search permission. Files are neither inspected nor changed, and symlinks are never followed. On Windows the read-only attribute itself blocks deletion, so every file and directory is made writable first. A delete that stops partway leaves a partial tree under the same journaled name, which a retry or recovery finishes deleting. An explicit forced removal snapshots the complete native view, or the complete OverlayFS private layer, before recording durable intent. The journal records both the force choice and snapshot. The snapshot includes Git HEAD, its symbolic branch, and structured staged index contents (including intent-to-add state). Index stat-cache refreshes do not change this consent. OverlayFS reads Git metadata through its private upper-layer pointer while the merged view is unmounted. Legacy content-only force snapshots cannot prove this Git state and are preserved for manual inspection rather than automatically completing a pending deletion. Riftri revalidates the combined snapshot at the final delete boundary, again at the quarantine path after the rename (a write landing between that check and the rename would otherwise be deleted with the quarantine), and during recovery; any later change stops the operation and is preserved. Only a still-present native view whose snapshot matches reaches Git's force removal. Missing-path metadata cleanup continues through ordinary Git safety checks, so recreating the destination cannot turn old force consent into deletion of a new path. A missing view plus missing Git registration is treated as an idempotently completed removal step. Inconsistent or changed paths are preserved for manual attention. Snapshot comparison is a final revalidation boundary, not a write lock against a process already holding an open file descriptor. ## Move-operation journal state machine Managed moves use separate journals under `moves/`: ```text intent-recorded -> worktree-moved -> add-journal-updated -> complete intent-recorded -> cancelled ``` Git performs the directory and administrative-metadata move. Recovery accepts only an intact registered source or an intact registered destination, preserves all inconsistent states, and atomically relocates the active add-journal reference. The source and destination must be on the same native COW-capable volume. If a move journal is incomplete, Riftri rejects removal, forced removal, compaction, and another move of that managed worktree. This keeps the worktree intact for `riftri repair`. Git refuses some moves only when asked, and refuses them every time, for example a worktree that contains submodules. When `git worktree move` fails and both paths still show an intact, registered source and an absent, unregistered destination, the journal advances to `cancelled` and the command reports Git's reason. Repair does the same for a journal left at `intent-recorded`, so a refused move never blocks the worktree. Any other state after a failure is preserved for manual attention. A destination inside the source worktree is refused before intent is recorded. ## Compaction-operation journal state machine Explicit compaction of a pristine native-COW view uses a separate journal under `compactions/`: ```text intent-recorded -> replacement-ready -> replacement-activated -> add-journal-updated -> complete intent-recorded | replacement-ready -> cancelled add-journal-updated -> restoring-original -> cancelled ``` Before recording intent, Riftri requires an active managed Git registration, the expected HEAD, and an empty structured status including ignored paths. It snapshots names, kinds, bytes, modes, symlink targets, Unix extended attributes, and Windows file attributes across the complete view. Unix setuid, setgid, and sticky bits are not represented by Git trees; compaction refuses entries carrying these special bits, including during recovery, rather than dropping them. Ordinary-mode snapshot compatibility is unchanged. macOS extended ACLs and ACL-wide inheritance flags also fail closed: Git cannot reconstruct them and they are not visible through xattr enumeration. Native ACL inspection does not follow symlink targets; unreadable ACLs stop the operation. This guard also preserves a quarantined view if an ACL appears before recovery cleanup, including for older compaction journals. Windows alternate data streams fail closed. Riftri then prepares or reuses the exact current-tree base, creates a fresh native COW replacement, and copies the real linked-worktree pointer into it. Immediately before the same-parent directory swap, Riftri revalidates registration, HEAD, strict cleanliness, and the snapshot. The old view is quarantined until the active add journal points at the new base; its snapshot is checked again before deletion. After that check, the verified view is renamed to `.riftri-compact-drop-` and deleted only under that name. A kill mid-delete therefore leaves a partial tree that recovery finishes deleting, instead of a quarantine that can never match its snapshot again. Recovery cancels intent-only work and restores a quarantined original when the replacement was not activated. A worktree that changes after its replacement was built, but before the swap, has not been touched, so the compaction is cancelled and only the replacement is removed. Once activation occurred it moves forward, updates the add journal idempotently, and removes only the exact journaled quarantine. A write that reached the old view after its last check is kept there: the compaction stays pending as recovery the caller completes, and the message names the kept view and says to copy what is needed into the worktree, delete it, and run `riftri repair`. A Git command that runs in the worktree just after the swap (a commit, a switch) acts on the old view, its working directory, while HEAD and the index move with it. The replacement then holds the pre-command files. When the old view changed, HEAD left the compacted commit or the index holds changes, and the replacement still matches its snapshot, Riftri records `restoring-original` and undoes the compaction: - it moves the replacement back to its staging name and renames Git's view into place; - it points the add journal at its original base and commit, which the journal records at intent; - it deletes the replacement and cancels. Each step is decided from the paths, so recovery finishes a kill at any point. When both views changed, neither replaces the other and both are kept. Journals written before the original commit was recorded keep the guidance above. A lock held by such a command does not fail the post-swap index refresh, which only updates Git's stat cache. A path that vanishes while compaction walks the worktree is reported as a change, not an I/O failure. Pending compactions protect both old and new bases from garbage collection. OverlayFS compaction remains separate future work because resetting a mounted private upper requires mount-identity-aware handling rather than a native directory clone and rename. ## Prune-operation journal state machine Managed-state pruning uses separate journals under `prunes/`: ```text intent-recorded -> git-metadata-pruned -> complete ``` Before invoking Git, Riftri verifies that every active managed view exists and is registered and that no add, removal, move, or compaction is incomplete. Repeating Git's prune after an interruption is safe because managed registrations are revalidated first. A prune inspects its repository once and requires Git to resolve the recorded directory to itself. Each verification pass, before the journal is written, under the metadata lock, and after Git's prune, then re-reads the registered state directories and lists Git's worktrees once, an inventory every state directory is checked against: a concurrent add can register either between passes. ## Base-collection journal state machine Garbage collection is read-only unless the user passes `--apply`. Each selected zero-reference base then uses a separate journal under `collections/`: ```text intent-recorded -> marker-removed -> base-quarantined -> complete ``` An operation may instead become `cancelled` if reference revalidation finds a live or incomplete add. Collection takes the same per-base lock as construction, reloads add and removal journals under that lock, and treats every non-rolled-back add without a completed removal as a reference. The completion marker is removed before the base is made writable for atomic quarantine, preventing another add from reusing a base once collection starts. Recovery deletes only the exact journaled quarantine path and leaves a newly rebuilt base untouched. `gc --apply` collects its planned bases in batches of up to 16, in sorted base order. It records each base's intent, then takes its lock; only the first lock of a batch waits, and a busy base starts the next batch, so collection never waits for a lock while holding another. Each step then reads the protecting add, removal, and compaction journals once for the whole batch: once after every lock is held, before the markers are removed, and once more after every marker removal, before any base is quarantined. Every decision is therefore made from journals read under that base's lock, and a reference that appears after a marker was removed is still found before quarantine. A reference recorded after the second read is handled as it always was: the add waits for the lock and rebuilds the base. A candidate that fails before its batch runs stops the run after the bases ahead of it are collected; a failure inside a batch leaves its other members with durable journals that the next `gc --apply` or `riftri repair` resumes, exactly as an interruption would. Before Milestone 2 performs its first mutation, journal persistence must write the intent first and replace each state atomically using a temporary file, `fsync`, rename, and parent-directory `fsync` where the platform supports them. On macOS the temporary file's flush is `F_BARRIERFSYNC`, which orders its content before the rename, and the directory's `F_FULLFSYNC` makes both durable before the write returns. See D046 in [decisions.md](https://github.com/assistant-ui/riftri/blob/main/docs/decisions.md). The persisted path encoding must round-trip platform-native paths, including non-UTF-8 Unix bytes. Recovery may repeat the cleanup associated with a recorded state, so every rollback action must be idempotent and validate its exact target. The per-operation journal remains the crash-recovery authority while an add is incomplete and the durable lifecycle record after activation. The APFS prototype builds bases only from exact Git objects through an isolated temporary index. A SHA-256 repository bucket, tree object ID, restricted checkout profile version, and same-volume state placement implement the base-key boundaries. A per-base file lock serializes construction, atomic rename exposes the finished tree, and a synced completion marker prevents reuse of a partially prepared base. Existing-base verification uses shared ownership of that same stable per-base lock. Each reader still recomputes the integrity digest, but concurrent cache hits no longer serialize their reads. A missing or incomplete base releases the read lock, obtains exclusive ownership, and revalidates all state before reuse or rebuilding. It never upgrades a held read lock in place. Collection remains exclusive and rechecks durable references, which protect live and incomplete adds after the preparation lock is released. No cache format, integrity check, or journal durability step changes. When a caller finds a cache miss but another builder completes the base while that caller waits for exclusive ownership, it does not retain the writer lock while hashing the newly completed tree. It releases exclusivity, reacquires the same stable lock for shared ownership, and recomputes the complete integrity marker there, allowing the other post-build waiters to verify alongside it. A miss after that ownership gap acquires exclusivity again and repeats the full revalidation before rebuilding, so collection and legacy-marker migration keep the same fail-closed behavior. Native `clonefile` is used for every regular file in an APFS view. An error is returned if APFS cannot clone; there is no byte-copy path. Directory structure, symlinks, and executable modes are preserved. The base is made read-only and the cloned view restores owner write permission before Git index synchronization. On APFS, permission restoration is part of the per-entry clone traversal rather than a second directory walk. It reads each cloned file's actual mode before adding owner write permission, leaves symlink targets alone, and uses the same directory mode policy and failure cleanup. Linux and ReFS also restore file permissions within clone workers using their already-open destination handles, avoiding a separate permission traversal. Immutable-base integrity verification, journal transitions, Git index initialization, and final clean-state checks are unchanged. On Linux, Riftri accepts Btrfs and reflink-enabled XFS only after an active `FICLONE` check succeeds on two unnamed files in the destination volume. The unnamed probe cannot leave a path behind after interruption. Every regular file in the view is then created with `FICLONE`; an unsupported ioctl aborts and rolls back instead of copying bytes. Linux uses the same immutable-base lock, add and removal journals, clean-state verification, recovery, accounting, and explicit garbage collection as APFS. OverlayFS capability probing creates lower, upper, work, and merged directories on the destination volume, then forks a short-lived child into a private mount namespace. Before probing, the child closes every inherited descriptor except its result pipe and immutable lower payload, including standard streams. The fork-only child cannot rely on close-on-exec flags: unrelated descriptors can pin another thread's mount or lock. It uses `close_range` where available and a raw, allocation-free `/proc/self/fd` scan otherwise; inability to isolate the descriptor table fails the probe before any mount. Parent descriptors remain untouched. The child mounts OverlayFS with explicit rootless-compatible options, verifies lower reads and private copy-up writes, unmounts, and reports the exact failing stage and operating-system error. After namespace isolation, the child resolves the private probe root as a native byte path in the new namespace and mounts fixed relative layer names. Commas, colons, spaces, long paths, and non-UTF-8 bytes in the destination therefore never become mount-option syntax. The caller's mount namespace is never changed and the parent removes the probe directory. A separate caller-namespace probe performs the same read, copy-up, isolation, and cleanup checks without namespace isolation. Core must require that stricter result before selecting a persistent backend, because success in an isolated child does not prove that ordinary Git processes can see a durable mount. Both persistent probe paths protect the lower test file first. A rootless mapped-root path must write through that mode directly; the ordinary helper path must restore normal checkout permissions through metadata-only copy-up before testing a data write. Neither probe activates a persistent worktree. The persistent storage primitive places `upper` and `work` under one journal-owned `overlays/v1/` directory and mounts the immutable base directly at the requested merged worktree path. Layer mount options use already-open `/proc/self/fd` directory references, so user-controlled commas, colons, spaces, long paths, and non-UTF-8 bytes never enter OverlayFS's option grammar. A successful mount returns its Linux boot ID, mount-namespace device and inode, and kernel mount ID. Recovery unmounts or deletes private layers only when that full identity is absent or matches; a different namespace or foreign mount is preserved for manual attention. This primitive deliberately mounts only in the caller's current namespace. Before mounting, the journal can retain the current boot and namespace context and the upper layer can contain a token-bound recovery marker. If the process exits after `mount(2)` but before the kernel mount ID reaches the journal, recovery accepts only an OverlayFS mount at the exact destination that exposes the same private marker. A different namespace or any marker mismatch remains foreign. Core selects OverlayFS only after the caller-namespace probe succeeds and reflink probing does not. Add moves the real linked-worktree pointer into the private upper, arms recovery, mounts the immutable base, persists exact mount identity, clears the marker, synchronizes Git's index, and verifies cleanliness. Removal rechecks Git cleanliness, unmounts only that identity, restores the pointer to the underlying destination, removes journal-owned private layers, and lets real Git remove the linked worktree. A crash at any persisted add or removal phase is recoverable in the same namespace, including the gap after `mount(2)` and before identity persistence. Explicit repair handles a missing active mount by first replacing stale boot/namespace identity with durable remount intent, then reusing the token marker around the new mount and identity write. Private upper contents survive this remount, including dirty user edits. A mount already occupying the destination remains foreign. Mounted moves fail before mutation. Ordinary unprivileged shells may cross a narrow, explicitly installed helper boundary. Riftri first tries the direct caller-namespace operation. If that is permission-denied, it accepts only an absolute helper path whose executable and every ancestor are root-owned, non-writable by group/others, and whose binary has the set-user-ID bit. The elevated executable cannot enter the normal CLI: it accepts only mount, exact identity-checked unmount, and journal-owned disposable work-directory reset requests, clears its environment, and revalidates that the lower, upper, work, layout, and merged directories are real paths owned by the requesting UID. Probe setup and copy-up verification remain in the unprivileged parent. Mounts use `nodev,nosuid`. Rootless namespaces store OverlayFS metadata in `user.overlay.*` with metacopy disabled; the elevated helper deliberately uses `trusted.overlay.*`, which the requesting user cannot forge, and enables metadata-only copy-up. The selected profile is part of the durable mount context and identity. On helper mounts, metadata-only copy-up restores checkout write modes while unchanged file data remains shared with the read-only base. Git, checkout materialization, agent processes, and ordinary reads and writes remain unprivileged. The helper is available system-wide once installed, but repository-local `riftri.enabled` consent still controls Git interception. If the direct path and helper are both unavailable, selection fails before journal or Git mutation and never falls back to a full copy. On Windows, Riftri accepts ReFS only after an active `FSCTL_DUPLICATE_EXTENTS_TO_FILE` check succeeds on two delete-on-close files in the destination volume and a private write leaves the source unchanged. The destination is temporarily sparse while it is sized so the operation does not allocate zero-backed clusters before cloning. Matching sparse and integrity-stream settings are restored, and aligned regions are cloned in requests below the ReFS 4 GiB limit. The final unaligned tail—at most one filesystem cluster—is copied because the Windows API requires cluster-aligned extents. Any aligned block-clone failure aborts and rolls back; it never triggers a full-file copy. The same immutable bases, locks, journals, recovery, clean removal, move/prune, and garbage-collection rules apply. Recovery validates every recorded cleanup path. Before removing a materialized incomplete view, it requires a complete byte/mode/symlink and directory-entry comparison with the immutable base, even when Git calls the worktree clean. Ignored files and unexpected empty directories count as private changes and are preserved. Native rollback repeats this check at the final removal boundary; OverlayFS checks the merged view before unmounting and revalidates its private layer afterward. Otherwise it retains the view and journal for manual attention. That retention is reported as recovery the caller completes, not a bare refusal: the message names the kept destination and says to copy anything needed out of it, delete it, and run `riftri repair`, which then finishes the rollback. A file written into the destination before the add returns is the usual cause. This stronger automatic-rollback rule does not change explicit removal's ordinary Git semantics. Storage accounting is derived from add/removal journals and completion markers. It reports active views, retained bases, per-base reference counts, logical bytes, and filesystem-allocated bytes. Allocated bytes can include shared native COW blocks and are not an exclusive-space measurement; the volume-delta benchmark remains the authoritative sharing check. Bases reaching zero references remain cached until the explicit garbage collector independently proves deletion is safe and records its intent. The collector is never part of normal worktree file access or Git command passthrough. `riftri repair` resolves the repository's state directory by default or accepts `--state-dir ` to select one explicitly. Both forms apply the same conservative journal recovery and do not infer or delete unjournaled paths. Status also inventories the state layout itself. Valid journals explain their temporary, staging, and quarantine paths; completion markers explain retained immutable bases; object-ID lock files explain coordination metadata. Anything else is reported as a state issue, as are active journals whose worktree, base, or completion marker is missing or unsafe. This diagnostic pass is read-only: neither repair nor garbage collection guesses that an unexplained path is safe to delete. ## Fast path Once a worktree is ready, Riftri is absent from ordinary file operations: ```text agent -> native filesystem -> shared base/private changes ``` It must not become: ```text agent -> userspace Riftri callback -> filesystem ``` For normal Git commands, the shim should directly execute the real Git process without opening Riftri state. Cached bases are built once and reused by concurrent worktree requests. ## Safety constraints - Never use a mutable working directory as a shared lower layer. - Never delete a worktree without respecting Git's dirty-worktree safeguards. - Never silently create a full copy when the selected policy requires COW. - Never run the complete Riftri process as root. - Never place writable Riftri lifecycle state on a network filesystem. - Treat submodules, sparse checkout, custom filters, and broader Git LFS forms as explicit compatibility features with safe fallback behavior. ## Initial delivery sequence 1. Read-only diagnostics and storage capability model. (complete) 2. Explicit APFS worktree creation on macOS. (complete) 3. Removal, recovery inventory, and disk accounting. (complete) 4. Process-scoped Git shim. (complete) 5. Linux reflink and OverlayFS backends. (complete) 6. Compaction and compatibility expansion. 7. Windows ReFS block cloning. (initial slice complete)