Skip to content

Latest commit

 

History

History
1189 lines (946 loc) · 58.6 KB

usage.md

File metadata and controls

1189 lines (946 loc) · 58.6 KB

Usage

Full reference for the FeanorFS CLI (feanorfs) — one binary for sync client and blob hub (feanorfs serve).

Install

macOS and Linux use one platform-aware installer:

curl --proto '=https' --tlsv1.2 -LsSf \
  https://raw.githubusercontent.com/rapm94/feanorfs/main/scripts/install.sh | sh

It installs the signed/notarized universal package on macOS. On Linux x86-64 or ARM64 it prefers a checksummed native .deb for Debian/Ubuntu or .rpm for Fedora/RHEL, letting apt, dnf, or yum resolve the tray dependencies. The installer verifies package identity and architecture and rejects embedded install scripts. A checked tar bundle is retained for custom install prefixes; older or unsupported releases fall back explicitly to the CLI-only installer.

Windows uses the checksummed, Authenticode-gated desktop installer:

irm https://github.com/rapm94/feanorfs/releases/latest/download/feanorfs-windows-installer.ps1 | iex

Both paths install the CLI and tray together when a trusted desktop product is available. After verification succeeds, an interactive desktop install opens the tray with equal native choices to Start Mirroring a Folder…, Join Another Computer…, or continue later. Both setup paths reuse the existing secure tray actions without opening Terminal. Existing workspaces never re-prompt. Root/headless sessions and FEANORFS_NO_LAUNCH=1 skip launch and print the CLI setup command instead. The tray delegates its folder picker to the same feanorfs start engine.

Normal setup

On the first computer, install FeanorFS, choose Start Mirroring a Folder…, and select the project. On another computer, choose Join Another Computer…, paste the one-time capability, and select the destination folder. The tray delegates both flows to the same secure CLI engine. When something is unhealthy, choose Check System Health…. The tray reads only check names and statuses from feanorfs --json doctor, maps them to generic human labels, and discards diagnostic messages that may contain local workspace identifiers or endpoints. Repair Mirroring is explicit and delegates to the existing feanorfs start -- <folder> lifecycle; it reuses the workspace's encryption and conflict safeguards while normal synchronization and service repair remain CLI-owned.

With no saved workspace or hub, start creates a private network hub in ~/.feanorfs/hub-data, enables native TLS and bearer authentication, installs the hub and workspace watcher as per-user login services, performs the initial sync, and registers the desktop tray when installed. Use Pair Another Computer… in the tray to display the one-time code in a native dialog without opening Terminal (or run feanorfs pair). On the other computer choose Join Another Computer…, paste the code, and select the destination folder. Both sides stay in the tray; the receiving capability crosses only bounded stdin into the ordinary start path.

The terminal equivalents are:

feanorfs start ~/projects/my-app
feanorfs start fnp1-… ~/projects/my-app

The normal path does not require Terminal, serve, scp, nohup, PID files, service commands, or credentials/capabilities in process arguments. Use start --host to explicitly make this computer the private hub when a different saved hub exists.

What FeanorFS is (and isn't)

FeanorFS is a working-directory mirror for developers who use more than one machine — think Dropbox for the folder you are actively working in, not for your git history. It keeps current files in sync across machines, including paths that are not in version control.

It is not version control. It has append-only recovery history, but no branches, commits, or content-merge UI. Use a VCS for intentional history and collaboration.

A synchronized folder is best understood as one shared work-in-progress overlay on top of independently managed Git baselines:

working tree = local Git baseline + shared FeanorFS WIP

Both machines keep normal Git clones. When Linux edits a tracked file, Git on macOS correctly shows that same file as modified — both clones are looking at the same unfinished change. That dirty state is expected, not a synchronization error. FeanorFS never hides, stages, commits, resets, pulls, merges, or synchronizes Git metadata; .git/.jj stay hard-excluded. Git remains the publication and history layer.

Git across machines

FeanorFS transports unfinished working state; Git publishes it. The two boundaries stay separate.

Both clones stay normal Git repositories

A build-only replica is allowed, but the primary model keeps a full Git clone on every machine: local history, blame, diff, editor integration, and the ability to become the publication machine. Removing .git from a secondary machine would prevent it from naturally becoming the active development environment.

Changing the active editing machine

  1. Let FeanorFS synchronization settle.
  2. Stop editing on the previous active machine.
  3. Continue editing the already-dirty shared WIP on the other machine.
  4. Do not create a handoff commit merely to change machines.

Publishing the shared WIP to Git

  1. Let agents finish or explicitly report that they are blocked.
  2. Choose one machine as the Git-history writer for this publication boundary.
  3. Review the complete shared WIP and resolve FeanorFS conflicts.
  4. Commit and push through ordinary Git.
  5. On the other machines, fetch and advance the corresponding Git baseline through ordinary Git.

If the synchronized tracked files exactly match the published commit and the remote commit is a fast-forward, you may perform a metadata-only mixed reset after verifying those conditions. Use the guarded form, substituting your real remote and branch for origin/main:

if git fetch origin &&
   git merge-base --is-ancestor HEAD origin/main &&
   git diff --quiet origin/main --; then
  git reset --mixed origin/main
else
  printf '%s\n' 'Baseline not advanced: inspect the Git divergence first.' >&2
fi

The reset is appropriate only when both checks succeed: the published commit is a fast-forward from your local HEAD, and your tracked working tree already equals that published commit. --mixed advances HEAD and the index while preserving working files and ignored/untracked files (.env stays in place). If either check fails, inspect the divergence with Git — FeanorFS never performs or suggests a destructive reset, and it never marks leftover tracked WIP clean or discards it.

Changing Git branches

A FeanorFS workspace is not a branch selector. Independent branch switches would make one clone's files appear as a large remote WIP change on the other clones. Before changing the shared baseline:

  1. Settle or preserve the current shared WIP.
  2. Pause editing and automatic synchronization as needed.
  3. Use Git to align the intended branch/commit on every participating clone.
  4. Resume FeanorFS only after the participating clones share the same baseline.

Teams that intentionally maintain simultaneous branch work should use separate folders and FeanorFS workspace identities; FeanorFS does not model Git branches.

Dedicated server and advanced self-hosting

Start the secure blob hub

feanorfs serve
feanorfs serve --gc-only --data-dir server-data
feanorfs serve --mdns

The hub listens on 0.0.0.0:3030 with native Rustls HTTPS by default. For a private/self-hosted hub it creates a durable CA, refreshes a leaf certificate for current network interfaces at startup, and prints an fnh1-… secure hub invite. The first client consumes that capability with one start command.

The data directory contains:

server-data/
├── auth-token     # generated hub bearer token (0600 on Unix)
├── db.sqlite      # metadata database
├── blobs/         # content-addressed ciphertext blobs
└── tls/           # private CA + refreshed server certificate (0700/0600 on Unix)

Back up the complete data directory for ciphertext and metadata. Separately, create an encrypted identity bundle so a restore keeps the same private CA and bearer token—existing clients then retain trust without receiving new invites:

# Stop the hub first; the command refuses an active data directory.
feanorfs serve recovery export ~/Backups/feanorfs-hub.recovery \
  --data-dir server-data

# After restoring db.sqlite and blobs/ (or onto a replacement host):
feanorfs serve recovery import ~/Backups/feanorfs-hub.recovery \
  --data-dir server-data

The passphrase is read from the terminal, never argv or an environment variable. The bundle uses Argon2id plus XChaCha20-Poly1305, is written atomically with 0600 permissions on Unix, and contains no leaf certificate; the next serve regenerates the leaf for the replacement machine. Import validates the CA/key pair and leaves a durable startup fence if interrupted; rerun the same import to resume. --replace is required to overwrite a different existing hub identity.

After a suspected CA-key or bearer-token compromise, rotate both while the hub is stopped. Rotation preserves db.sqlite, blobs, opaque heads, manifests, and all E2EE ciphertext, but intentionally invalidates every old client connection:

feanorfs serve recovery rotate ~/Backups/feanorfs-hub-rotated.recovery \
  --data-dir server-data

The command generates a fresh private CA and 256-bit token, requires a new Argon2id/XChaCha20-Poly1305 recovery bundle to be written successfully first, requires that backup to live outside the hub data directory, then fences the multi-file identity replacement so the same command and bundle path can resume after interruption. Restart the hub to emit its replacement fnh1-… invite. On every existing client, run:

feanorfs start fnh1-… /path/to/existing/folder

The client authenticates the replacement HTTPS CA/token and existing opaque workspace head before changing connection trust. Its workspace ID, E2EE key, files, refs, and encrypted history remain unchanged. Do not restore the old identity after compromise; retain the new recovery bundle and its passphrase in separate secure locations.

Flag Description Default
--token <TOKEN> Rotate/set the persisted bearer token. --password is an alias. generated 64-hex token
--allow-open Disable bearer authentication off; development only
--allow-http Disable native TLS off; reverse proxy/development only
--tls-cert, --tls-key Use a supplied PEM certificate chain and private key auto-generated private CA
--tls-ca Private CA certificate to include in hub/workspace invites none for public CA chains
--public-url URL embedded in the fnh1 hub invite first non-loopback IPv4 address
--show-invite Expose the secret-bearing hub invite when stdout is redirected off
--port <PORT> Port to listen on. Use different ports for multi-instance deployments. 3030
--data-dir <DIR> Data directory for SQLite DB and blob storage. Each instance should have its own. ./server-data
--mdns Enable mDNS service advertisement for LAN discovery off
--relay Enable public opaque routes for fnp2 pairing and inner-TLS private-hub tunnels (--pair-relay compatibility alias) off
--gc-only Run blob/tombstone GC once and exit (no HTTP listener) off
--gc-interval <SECS> Periodic GC while serving (feanorfs serve only) off

All flags can also be set via environment variables: FEANORFS_TOKEN, FEANORFS_PORT, FEANORFS_DATA_DIR.

Internet deployment

Native TLS can use a publicly trusted certificate directly:

feanorfs serve \
  --tls-cert /etc/letsencrypt/live/sync.example.com/fullchain.pem \
  --tls-key /etc/letsencrypt/live/sync.example.com/privkey.pem \
  --public-url https://sync.example.com

A TLS-terminating proxy remains supported, but disabling native TLS must be explicit and the HTTP listener must not be exposed directly:

feanorfs serve --allow-http
caddy reverse-proxy localhost:3030

mDNS is off by default — it's only useful on LAN and can't cross routers.

Multi-instance deployment (SaaS-ready)

feanorfs serve --port 3001 --data-dir /data/alice
feanorfs serve --port 3002 --data-dir /data/bob

Caddy or another gateway can route subdomains to private --allow-http ports:

alice.feanorfs.app { reverse_proxy localhost:3001 }
bob.feanorfs.app   { reverse_proxy localhost:3002 }

Each instance is fully isolated: separate SQLite DB, separate blob storage, separate auth token. This is the deployment model for the managed SaaS — same binary, no code changes needed.

LAN deployment

For local-only setups, serve prints a secure fnh1 capability containing the URL, bearer token, and public CA certificate. Paste it into the first client:

feanorfs start fnh1-… ~/projects/my-app

mDNS advertises only the HTTPS endpoint and public CA fingerprint. Because mDNS is unauthenticated, start --lan refuses private-CA trust-on-first-use and asks for the fnh1 capability. Publicly trusted HTTPS hubs can still be discovered. Automatic private hubs advertise a CA-derived feanorfs-….local hostname whose addresses track interface and DHCP changes. The capability-pinned public CA, not mDNS, remains the trust anchor.

Log verbosity can be tuned via RUST_LOG (see Environment below).

Client

Visible commands

Command Purpose
start Create, join, or resume a workspace — then sync and enable automatic background sync
stop Stop mirroring a folder and remove it from the tray while preserving files and encrypted setup
sync Upload/download changes; enters watch mode by default
status Read-only diff vs the mirror
hydrate / cat Materialize lazy placeholders / print a file
summary What changed since your last session
config Inspect workspace config (--key for full key + invite)
recovery Export or restore a passphrase-encrypted workspace access kit
service Inspect or repair automatic per-workspace background sync
pair Share this configured workspace with another computer using a single-use LAN code
doctor Troubleshoot lifecycle/config or collect privacy-safe migration evidence
update Check the official stable release without downloading or installing it
serve Run a blob hub
migrate Upgrade legacy workspaces to format v3 encrypted snapshots
agent Isolated agent workspaces (status, spawn, land, …)
conflicts List and resolve sync conflicts (keep, show)

Hidden aliases remain for scripts: setup, init, join, attach, connect, push, pull, watch, show-key, workspaces, prune-ignored, events, mcp, agent check, agent commit, conflicts open, conflicts history.

start — create, join, or resume (recommended entry point)

feanorfs start                                    # resume existing workspace
feanorfs start ~/projects/my-app                  # first use: host privately; later: resume
feanorfs start --host ~/projects/my-app           # explicitly host on this computer
feanorfs start --relay https://relay.example ~/projects/my-app # private hub reachable off-LAN
feanorfs start fnh1-… ~/projects/new-app          # create from secure hub invite
feanorfs start fnh1-… ~/projects/existing-app     # refresh trust after hub identity rotation
feanorfs start https://my-server.com:3030         # create on server
feanorfs start 127.0.0.1:3030                     # unambiguous bare host:port (https:// added)
feanorfs start fnr1-…                             # join from invite
feanorfs start fnp1-… ~/projects/my-app           # secure LAN pair + join + run at login
feanorfs start fnp2-… ~/projects/my-app           # secure off-LAN pair through embedded relay URL
feanorfs start fnr1-… ~/projects/my-app           # join into folder + run at login
feanorfs start --local                            # embedded local hub
feanorfs start --local --workspace existing --encryption-key <KEY>
feanorfs start --lan                              # mDNS discovery + create
feanorfs start --no-watch                         # sync once, no watch loop
feanorfs start --foreground                       # keep watcher attached to terminal
Flag / positional Description
target (first positional) Server URL, unambiguous localhost/IP/dotted host:port, fnh1-… hub invite, fnp1-… LAN or fnp2-… off-LAN pairing capability, fnr1-… workspace invite, or folder path. Use https:// for a custom single-label server name.
folder (second positional) Destination folder when the target is a server, pairing code, or invite (default: current directory)
--workspace, -w Explicit workspace ID for advanced/manual setup; new mirrors receive an opaque random fsw1-… ID when omitted
--encryption-key Manual re-link with the existing 64-character lowercase-hex recovery key (requires --workspace; human passphrases are rejected before configuration is written)
--server-token, --token Server access token
--lan Discover server via mDNS
--local Embedded in-process hub (no remote server)
--host Create/reuse this computer's secure private hub and run it automatically at login
--relay <HTTPS-URL> Create/reuse this computer's private hub and persist an outbound opaque relay route (FEANORFS_RELAY)
--no-watch Sync once and exit (no watch loop)
--foreground Keep the watcher attached to the terminal instead of installing a user service

Hidden init / setup / attach / join remain for scripts: they configure only. Use feanorfs start for the full onboarding flow: initial sync, automatic service, and the desktop tray when installed.

The automatic private hub prefers port 3030. On a fresh profile, if another application already owns that port, start scans a bounded stable fallback range, selects an available port, and persists it in the protected hub data directory; pairing, mDNS, the tray, diagnostics, and login restarts reuse that endpoint automatically. Existing hubs retain their historical port. Explicit feanorfs serve --port behavior is unchanged.

Each implicitly created folder receives its own opaque random workspace ID. This prevents a second feanorfs start ~/another-folder from accidentally joining the first folder's mirror. Pair codes and full invites preserve the existing workspace ID; --workspace is reserved for explicit/manual setups.

When the destination folder is already configured, an fnh1-… target refreshes only its hub URL, bearer token, and TLS trust. The command requires HTTPS and an authenticated token, verifies the replacement CA and existing opaque workspace head before persisting anything, then preserves the workspace ID, E2EE key, local files, Merkle refs, and encrypted history. If authentication fails, the old connection remains intact. This is the client re-pairing path after an intentional hub identity rotation; offline server-side rotation remains an advanced operation that must preserve the hub database and blobs.

By default, start installs one credential-free service command per workspace. The service receives only the canonical folder path and reads the protected workspace configuration itself. Signed macOS releases use Keychain; Windows uses Credential Manager and Linux uses Secret Service when available. JSON then contains only a random credential reference. Unsigned macOS/source builds or unavailable stores retain the atomic private-file fallback (0600 on Unix). Set FEANORFS_CREDENTIAL_STORE=file only for an intentional headless fallback. Services use launchd on macOS, the native user service manager on Linux, and Task Scheduler on Windows.

The first-machine path also installs one global private-hub login service. Its command contains only the protected hub data-directory path. It generates or reads the bearer token and TLS material inside that directory, runs periodic retained-snapshot GC, and never receives credentials, capabilities, or recovery passphrases through argv or the environment. The host workspace uses loopback; exported invites use the managed hub's stable CA-bound .local hostname after verifying that the public CA matches the durable CA on disk. Existing numeric LAN configurations migrate only after an authenticated, CA-verified probe.

stop — stop mirroring without deleting the folder

feanorfs stop                       # current folder
feanorfs stop ~/projects/my-app     # explicit folder
feanorfs --json stop ~/projects/my-app

stop removes the workspace's automatic sync service and tray registration, then selects the next recent workspace in the tray. It preserves working files, .feanorfs encrypted setup, OS-stored credentials, remote encrypted snapshots, and any private hub that other folders or computers may still use. Open the folder and run feanorfs start to resume. If an unmanaged feanorfs sync process is running in a terminal, stop refuses until that process exits rather than claiming that mirroring stopped while it is still active.

If a listed folder was moved, deleted, or is on a disconnected drive, the tray shows it as unavailable and disables switching to it. Remove Unavailable Folders… asks for confirmation—so a temporarily disconnected drive can be reconnected instead—then removes only those tray-list records. It does not delete files, encrypted setup, credentials, services, hub data, or remote snapshots.

retire — explicitly remove leftover workspace state (hidden)

feanorfs retire ~/projects/old-app --grace 604800   # tombstone; 7 days default
feanorfs retire ~/projects/old-app --grace 0        # quarantine immediately
feanorfs retire --sweep                             # process expired tombstones only

When a mirrored folder is deleted, its encrypted global workspace state stays under ~/.feanorfs/workspaces/ because FeanorFS never infers orphanhood. retire <folder> explicitly tombstones that state: the request records the slot's filesystem identity and canonical path, refuses when the folder is still live or another process holds the slot, and keeps the bytes in place for a grace period (7 days by default) so the decision is reversible. After grace, a sweep moves the slot into ~/.feanorfs/quarantine/; after the quarantine retention window (24 h, FEANORFS_RETIRE_QUARANTINE_SECS override), it is deleted only after re-verifying that the recorded folder is still gone under an exclusive state lease. State without a recorded identity or without an explicit tombstone is never retired or deleted.

recovery — encrypted workspace access backup and restore

From a mirrored folder:

feanorfs recovery export ~/FeanorFS-recovery.fnrk
feanorfs recovery export --replace ~/FeanorFS-recovery.fnrk

Export prompts twice for a passphrase of at least 12 characters. The resulting versioned kit contains the full portable workspace capability—server location, workspace ID, bearer token when present, E2EE key, public private-hub CA, and optional opaque relay route—only as authenticated ciphertext. Argon2id (64 MiB, three iterations, one lane) derives a key for XChaCha20-Poly1305. Unix writes are atomic and 0600; an existing destination is refused unless --replace is explicit.

On another computer:

feanorfs recovery import ~/FeanorFS-recovery.fnrk ~/projects/my-app

Import prompts once, decrypts and validates in-process, and makes no workspace or global configuration write when the passphrase is wrong, the kit is modified, or its E2EE key is invalid. A successful import delegates to the ordinary start path: initial sync, protected credential storage, automatic workspace service, and tray registration all remain unified. --no-watch and --foreground retain their normal advanced meanings.

The tray's Recovery → Export Encrypted Recovery Kit… and Restore From Recovery Kit… actions use native file and masked-passphrase dialogs. The passphrase crosses only a bounded local stdin pipe to the CLI child; neither it nor the decrypted capability appears in process arguments, environment variables, or logs. The tray owns no cryptography.

A workspace kit is an access backup, not a copy of file blobs. The hub must still be reachable and retain the encrypted snapshot. A self-hosted deployment also needs an opaque hub-data backup and, separately, feanorfs serve recovery export for its private CA and bearer-token identity. Losing the workspace-kit passphrase is unrecoverable without another configured client or invite.

service — automatic sync lifecycle

feanorfs service status [folder]
feanorfs service install [folder]
feanorfs service stop [folder]
feanorfs service start [folder]
feanorfs service uninstall [folder]

These are recovery and diagnostics commands; normal setup and offboarding use feanorfs start and feanorfs stop. Advanced service uninstall removes automatic startup without changing tray registration or deleting the workspace, encryption key, or mirrored files.

FeanorFS runs exactly one per-user background job (macOS/Linux com.feanorfs.agent, Windows Task Scheduler FeanorFS\Agent) that supervises the private hub, every registered workspace watcher, and the tray, so consumer background-item lists show a single entry. service install|start registers a folder with that job; service stop pauses it; service uninstall forgets it. Upgrades migrate legacy per-component jobs into the supervisor automatically.

Machine A — create and host privately:

feanorfs start ~/projects/my-project --workspace my-project
# → fnr1-… invite + encryption key on clipboard

Machine B — join:

feanorfs start fnr1-...

Interactive setup prints a fnr1-… workspace invite. Redirected output hides recovery keys and capability invites; use explicit config --key to export them. Local-hub workspaces (--local) are not portable via invite—share the embedded data through the workspace hub directory shown by feanorfs config first.

pair — add another computer on this LAN

On an already configured and synchronized computer:

feanorfs pair                    # five-minute, single-use code
feanorfs pair --expires 120      # 30–900 seconds

The command copies an fnp1-… code and waits. On the other computer, choose Join Another Computer… in the tray, paste the code, and choose a folder. The terminal equivalent is:

feanorfs start fnp1-… ~/projects/my-app

The second command discovers the matching ephemeral session over mDNS, authenticates the code with SPAKE2, decrypts the full fnr1 invite (including its public hub CA) with ChaCha20-Poly1305, performs the initial sync, installs background sync, and returns. mDNS never contains the pairing secret, server token, E2EE key, workspace ID, or hub URL. A code works once, permits at most three online attempts, and cannot cross routed networks; configure an owned private hub with start --relay, use pair --relay as the combined configure-and-pair shortcut, or transfer a full invite outside one LAN.

When this computer owns the automatic private hub, the pairing payload uses its stable CA-bound .local hostname. The hub advertises current interface addresses automatically, so moving a laptop or receiving a new DHCP lease does not change the invite, CA, or bearer token. Custom loopback hubs retain a connection-local IP fallback.

Off-LAN pairing and private-hub transport

A public relay enables both bounded pairing rendezvous and opaque inner-TLS tunnels. It can run independently from the hub:

feanorfs serve --relay --public-url https://relay.example.com

On a computer that owns an automatic private hub, configure it through the same consumer entry point:

feanorfs start --relay https://relay.example.com ~/projects/my-app

This generates a random 256-bit route, stores it in protected workspace/global config plus atomic 0600 hub-local state, and restarts the credential-free hub login service. The hub keeps four outbound WSS offers available. Pair Another Computer… and feanorfs pair then reuse the stored relay automatically; pair --relay <URL> remains a one-step way to configure it while opening a pairing session.

The command and tray emit an fnp2-… capability. It contains a public 128-bit session ID, relay URL, and an 80-bit single-use secret; the long value stays in the clipboard in the tray instead of filling the native dialog. On the other computer the ordinary entry point is unchanged:

feanorfs start fnp2-… ~/projects/my-app

Both clients connect outbound over WSS. The relay keeps at most 1,024 pending offers for fifteen minutes, forwards at most eight 16-KiB binary frames during a 30-second exchange, and stores no payload. It sees only the public session ID, network metadata, timing, and opaque frame sizes. SPAKE2 prevents an offline test of the pairing secret; ChaCha20-Poly1305 and explicit key confirmation protect the full invite.

For synchronization, the remote client binds an ephemeral loopback bridge while retaining the private hub's CA-bound hostname as TLS SNI. The relay forwards only binary chunks from that inner Rustls stream. It allows at most 4,096 pending host offers, eight per route, 1,024 active tunnels, 64-KiB frames, 16 GiB per connection, and 24 hours per connection. Readiness uses WebSocket control frames before the TLS ClientHello is consumed, so a startup race retries without weakening TLS.

The tunnel route is an unguessable reachability capability, not hub authentication. A relay or anyone who learns it can deny service and observe IP addresses, timing, connection counts, and sizes. They still cannot complete the inner TLS/bearer handshake, read or modify accepted traffic, or learn API paths, workspace IDs, object names, tokens, or file ciphertext. There is no deployed default relay yet, and direct NAT traversal remains separate work. For the non-root, read-only-capable OCI deployment, reverse-proxy logging constraints, and provenance verification, see Deploy the opaque relay.

config — show configuration

feanorfs config
feanorfs config --key    # full E2EE key + invite, copied to clipboard

Prints global connection (~/.feanorfs/global.json) and the private workspace config under ~/.feanorfs/workspaces/<opaque-id>/. Default output truncates the key; --key shows the full value.

doctor — diagnose connection issues

feanorfs doctor
feanorfs --json doctor
feanorfs doctor --migration-report
feanorfs --json doctor --migration-report

Desktop users can run the same lifecycle checks from Check System Health… in the tray. Healthy and warning results are read-only. When a required check fails, the dialog can explicitly retry the existing secure start lifecycle; FeanorFS never resolves conflicts automatically.

Runs non-destructive health checks for workspace/global config, E2EE, format-v3 encrypted Merkle snapshots, the automatic workspace service, tray registration, authenticated mirror reachability, local sync state, executable version coherence (all registered background-job executables must report the same version as the CLI), throttled release awareness, and—when this computer owns the private hub—the hub login service. It therefore catches a workspace that is reachable but no longer syncing automatically, or a hub that works only because a manual process happens to be running.

Human output states what failed, what was preserved, and the next recovery command. --json returns { "ok": boolean, "checks": [...] }; each check has a stable name, status (ok, info, warning, or failure), message, and optional action. No key, bearer token, invite, or recovery secret is emitted.

--migration-report is a separate, offline evidence mode for any future, separately approved legacy-format retirement decision. It reads only format_version from the current and recent workspace config files, deduplicates folders, and reports aggregate v1/v2/v3, unsupported, and unreadable-or-missing counts. It does not open OS credentials, contact a hub, or emit paths, labels, workspace IDs, endpoints, credential references, relay routes, keys, tokens, or capabilities. A profile is marked ready only when it contains at least one workspace, the recent-workspace registry is readable, and every entry is a readable supported v2/v3 config. This local result is field evidence, not permission by itself to remove legacy decryption. JSON includes "report_version": 1 so collectors can reject incompatible schemas.

update — check the official stable release

feanorfs update
feanorfs --json update
feanorfs update --periodic        # throttled: reuse the last result for 24 h

The command makes one bounded, eight-second, HTTPS-only, no-redirect request to GitHub's official latest-release API. It caps the response at 256 KiB, parses versions with the standard semver crate, rejects drafts, prereleases, malformed tags, and every release URL except the exact matching github.com/rapm94/feanorfs/releases/tag/v… page. JSON returns status (up_to_date, update_available, or development_build), current/latest versions, and the validated public release URL.

update --periodic stores the typed result in the secret-free per-machine ~/.feanorfs/update-state.json and reuses it for 24 hours, so scheduled checks never hammer the release API. The tray's periodic offer and doctor's update_available check read the same cached result and report the same version. doctor also reports executable-version divergence across registered background jobs with one repair action.

The tray's Check for Updates… delegates to this command and repeats the exact URL check before offering Open Release Page; its periodic daily check uses --periodic and only ever opens that page after an explicit choice. Neither surface downloads, installs, or executes an artifact. Package signatures, checksums, attestations, and the platform installers remain the release trust boundary.

status — show local vs. remote differences

feanorfs status

Read-only scan + server diff. Does not modify files.

sync — upload, download, watch

feanorfs sync [--lazy] [--no-watch]
feanorfs sync --up [--no-watch]      # upload only
feanorfs sync --down [--lazy]        # download only
Flag Description
--up / --down One direction only (replaces hidden push / pull)
--lazy Metadata only; 0-byte placeholders
--no-watch Single pass and exit (scripts/CI)

By default, sync enters real-time watch after the initial pass. Debounced 500 ms.

hydrate / cat

feanorfs hydrate [PATH]     # materialize lazy placeholder(s)
feanorfs cat <PATH>         # print file (auto-hydrates)

summary — session catch-up diff

feanorfs summary [--summarize] [--no-remember]
Flag Description
--summarize Pipe paths/metadata to FEANORFS_SUMMARY_CMD (default feanorfs-llm)
--no-remember Do not update the session baseline

FEANORFS_SUMMARY_CMD must be a path to an executable file (e.g. /usr/local/bin/feanorfs-llm). It is invoked directly — not interpreted by a shell — so arguments like --model must be wrapped in a shell script.

Upgrade to format v3 with migrate

feanorfs migrate [--rekey]

Re-seals legacy blobs with authenticated encryption, creates the initial snapshot tree, sets the workspace head, and bumps format_version to 3. A durable migration journal and server fence make retries safe after interruption. Run the same command again to resume. Use --rekey for a historical human-chosen key; it creates a canonical 256-bit replacement. Land or clean agent workspaces before using --rekey.

Inspect and restore snapshots with log and undo

feanorfs log [--limit 20]
feanorfs undo snapshot_id

log prints the short ID, age, author, changed-path count, and message. Add --json for full IDs and structured paths. Recognized agent signals render concisely (signal <from> -> <to> <kind> about <short-id>: <body>) instead of raw protocol JSON; the --json history shape is unchanged. undo accepts a reachable full ID or an unambiguous prefix with at least eight characters, then records the restored tree as a new snapshot.

Global flags

Flag Description
--json Structured JSON. status includes mirror_state for tray clients.

agent — isolated workspace copies

feanorfs agent                    # list agents (one-line state when online)
feanorfs agent status [NAME]      # list all, or preview one agent
feanorfs agent spawn <NAME> [--no-sync] [--replace]
feanorfs agent refresh <NAME> [--replace]
feanorfs agent land <NAME> [--clean] [--propose]
feanorfs agent clean <NAME>
feanorfs agent run <NAME> -- <COMMAND> [ARGS...]
feanorfs agent send <TO> --kind <request|status|result|blocked> [--about <SNAPSHOT>] [--reply-to <MSG-ID>] [--from <NAME>] "<BODY>"
feanorfs agent inbox [--for <NAME>] [--after <HEAD>] [--limit <N>]
Subcommand Description
status List all agents (enriched when server reachable; names-only offline). Hidden agent list returns legacy JSON {"agents": ["name"]}.
spawn APFS clonefile/copy snapshot with server base hashes
refresh Pull cloud changes the agent hasn't touched. --replace discards agent-local edits after preserving them as a parent snapshot.
land Apply clean work, upload, register conflicts
clean Remove agent dir and snapshot rows
run Run a command in the agent dir with continuous reconciliation for the command's lifetime — not a sandbox. Sets FEANORFS_AGENT, FEANORFS_AGENT_DIR, and the absolute shared control root in FEANORFS_WORKSPACE_ROOT.
send Publish one encrypted signal tied to a snapshot; sender defaults to FEANORFS_AGENT, then human
inbox Read signals addressed to you (or * broadcasts); pass --after the previous cursor for new-signal deltas; a reset cursor means older signals may have been missed

Isolation caveat: data isolation only — see threat-model.md.

Agent signals are encrypted snapshots with no file-tree changes: they create no project files, never dirty Git, and keep the hub opaque. See agent-communication.md for the protocol, JSON contracts, and safety rules.

Continuous active agents

While an agent is active, FeanorFS reconciles it automatically. Activation is explicit and process-lifetime: feanorfs agent run <name> -- <command>, or an enabled configured runner for that agent. agent spawn alone never activates anything, and upgrading FeanorFS never activates a dormant agent.

agent edits files
    -> 500 ms quiet-period debounce
    -> automatic land (clean=false, propose=false) advances shared WIP
    -> other active agents wake on the opaque head change
    -> automatic refresh applies paths they have not touched
  • No transfer commands. An active agent never runs sync, push, pull, land, or refresh during normal operation; those remain diagnostics and recovery tools. No Git repository or Git command is required or used.
  • Shared WIP, not approval. Every automatically published snapshot is unfinished transport state. Only an explicit result signal referencing a settled snapshot claims that code was inspected or tested.
  • Conflicts stay explicit. Overlapping edits pause automatic mutation and materialize .original/.local/.cloud legs in private global state. Resolve with feanorfs conflicts keep; FeanorFS never merges content.
  • Signals wake, never activate. A snapshot-linked ffmsg1 signal wakes event/runner consumers immediately, but never starts a model.

While the command is alive, watch the live projection:

feanorfs agent status <name>    # "Live: idle head=… settled=…" when active
feanorfs events                 # agent_reconcile_started/reconciled/deferred/attention

On child exit FeanorFS drains the final burst and makes one bounded final reconciliation attempt, then prints the honest outcome:

Agent 'worker' settled (snapshot <64-hex>).        # everything reconciled
Agent 'worker' finished offline: …                 # work preserved; reactivate to retry
Agent 'worker' needs attention (pending_conflicts) # explicit resolution required

An interactive controller stops with its child, including after an offline final attempt. Run agent run again after connectivity returns to reconcile the preserved work. An enabled configured runner continues retrying only while its worker/controller remains active.

Failure semantics are bounded: filesystem bursts coalesce into one dirty generation, one operation mutates one agent at a time, retryable transport failures retry with backoff, and conflicts/corrupt state/unsafe layouts stop for attention instead of looping. If a duplicate owner is started, the second one fails before any mutation; exiting the owner is the rollback and always preserves the worktree, base ref, snapshots, and credentials.

Head-change waiting rides the existing authenticated GET /api/head route with optional bounded wait parameters; old hubs ignore them, and clients detect that and keep bounded periodic polling — never a busy loop. Continuous reconciliation requires a format-v3 workspace.

Agent runner

The optional agent runner is the local delivery path for one already-spawned agent. This is the canonical operator runbook. It does not replace ffmsg1 signal transport: an ordinary signal remains passive until an external orchestrator polls it or an operator explicitly configures and starts this local runner. See agent communication for the delivery sequence and agent API for its current JSON projection.

Run the controller from the configured workspace root:

feanorfs agent runner setup <agent> [--timeout <seconds>] -- <program> [args...]
feanorfs agent runner start [--foreground]
feanorfs agent runner stop
feanorfs agent runner status
feanorfs agent runner reset --discard-pending
feanorfs agent runner remove --discard-pending

Before the first setup, the workspace must already be a configured format-v3 workspace with a current snapshot head, and <agent> must be a real agent already created by feanorfs agent spawn with its base snapshot. There is exactly one configured runner per workspace. setup resolves <program> through PATH to a canonical executable, records that executable and its fixed arguments, and, for a new runner, checks the current head. It leaves the runner disabled and unregistered and does not launch a child. The default timeout is 3600 seconds; accepted values are 60 through 86400 seconds.

Run setup again with the same agent to replace the program, fixed arguments, or timeout while preserving runtime state such as queued request IDs and checkpoints. It first disables and unregisters supervised execution, and it refuses to reconfigure a launching or running request. It still does not launch work. To configure a different agent, first run remove --discard-pending for the existing runner.

Start, foreground, stop, and status

Normal start enables the runner, registers it with the existing FeanorFS supervisor, and waits for that supervised worker to be running. The supervisor owns restart of an idle worker; an idle worker restart is not itself an attention condition.

start --foreground first disables and unregisters background execution, then runs the same loop in the invoking terminal. That foreground runner is terminal-owned: feanorfs agent runner stop controls the supervised runner and does not terminate a foreground loop. On Unix, Ctrl-C or SIGTERM to the foreground process requests shutdown; on Windows, Ctrl-C or Ctrl-Break does the same. The runner then tears down the current child and its descendants, publishes a correlated cancellation fallback when it can, and exits. A signal or terminal event does not make a request eligible for automatic replay.

stop disables supervised admission and removes the runner from the supervisor registry while preserving its configuration, queued request IDs, active checkpoint, and agent workspace. status reports the redacted local runner and supervisor state; it is safe to use while the hub is unavailable.

When a supervisor has authority for this workspace, stop waits for a durable workspace-scoped reconciliation acknowledgement: the current registry generation/digest must show this runner removed, and the acknowledgement must belong to the live supervisor's exact native process identity. If the supervisor has crashed, recovery uses the recorded native process-start identity and, where the platform exposes it for orphan matching, the exact executable and worker arguments before touching an orphan. An identity that cannot be proved is left alone and the operation fails closed; retry after repairing the supervisor state. A fresh or already-disabled, unregistered runner with no supervisor authority has no child that could acknowledge the change, so an unregistered, authority-free idempotent stop and fresh setup skip this impossible acknowledgement wait. A stale registry entry, status, or acknowledgement authority is not treated as fresh and remains fail-closed.

The supervisor restarts hub, watcher, and runner workers after clean exits or crashes with bounded backoff. On a new runner session, a persisted launching/running checkpoint is marked ambiguous_execution; the request is not relaunched. Stop/restart recovery therefore preserves the checkpoint and asks for explicit inspection and discard rather than guessing whether a child ran.

What the runner executes

The runner reads the normal agent inbox, so an operator can still read broadcasts with agent inbox. For execution, however, it admits only a request whose recipient exactly equals the configured agent. Broadcasts, other message kinds, duplicate request IDs, and already-completed request IDs are ignored by the runner. Accepted requests run one at a time.

For each accepted request, the runner refreshes the agent worktree and starts a fresh child with the fixed configured executable and arguments. The child runs in that agent's worktree, inherits the normal environment plus FEANORFS_AGENT, FEANORFS_AGENT_DIR, and FEANORFS_WORKSPACE_ROOT, and receives one bounded RunnerInvocation JSON document on stdin followed by EOF. Its stdout and stderr are discarded, and its exit status is not a terminal reply.

The child must publish one terminal result or blocked signal from the configured agent to the requester's from identity, with reply_to set to the request message ID. Set about_snapshot to the snapshot actually inspected; do not claim a different snapshot. A status signal is optional. The one-terminal child contract is not an exactly-once transport guarantee. When a child process cannot start, its invocation cannot be delivered, it exits without a correlated terminal, it times out, or it is cancelled, the runner tries to publish a generic correlated blocked fallback. If it cannot establish that one correlated terminal was observed or published, it stops for attention instead of replaying the request.

Every child is owned as a process tree. Unix (including macOS) starts it in a fresh process group and uses bounded TERM then KILL teardown. Windows creates it suspended, assigns it to a private Job Object configured for kill-on-close, verifies membership, and only then resumes it. Timeout, cancellation, and direct-child exit all tear down admitted descendants before the runner reports the cycle complete. This is process ownership, not process sandboxing; agent run still isolates data only.

Attention and recovery

Runner runtime checkpoints retain cursors, request/session IDs, phases, timestamps, and terminal IDs—not request bodies or child output. Public status also redacts configured command/arguments and process metadata.

The runner stops and requires operator recovery for exactly these attention reasons:

Attention reason Meaning Recovery
cursor_reset The bounded inbox delta may have omitted older signals. Inspect the agent worktree, stop the supervised runner, then reset with explicit discard.
pending_overflow A bounded direct-request queue could not admit the batch. Inspect, stop, then reset with explicit discard.
ambiguous_execution A persisted launching or running execution was found after its worker/session stopped, so it may already have run. Inspect, stop, then reset with explicit discard; do not relaunch it.
delivery_unknown The runner could not establish a correlated terminal result or blocked delivery. Inspect, stop, then reset with explicit discard; do not retry it automatically.
preparation_failed Local refresh/preparation failed before the child launch. No child was started, but the pending request and cursor checkpoint remain preserved. Repair or inspect local workspace/agent state, stop the runner, then run reset --discard-pending (or remove with explicit discard) if the request should be abandoned; there is no automatic replay.

Generic child failures do not automatically require attention when the runner can publish and observe its generic blocked fallback. In contrast, only a persisted launching or running execution becomes ambiguous_execution after interruption; an idle worker can be restarted by the supervisor.

reset --discard-pending is available only when the runner is stopped. It reads the current workspace head, advances the runner cursor to that head, and discards pending, active, and attention state. Discarded requests are never replayed. remove --discard-pending also requires the explicit discard flag; it removes runner state only and preserves the agent worktree, base snapshot, and runtime. Remove a configured runner before agent clean, agent spawn --replace, or agent land --clean for that agent.

There is no exactly-once delivery guarantee: inbox reads can redeliver, signal history can be bounded or reset, and a terminal can be published but not be observed by this runner. Durable IDs suppress ordinary duplicate admission; they do not prove that an external child ran once. Treat every attention reason as a human decision point and use explicit discard/reset before resuming.

conflicts — list, compare, resolve

feanorfs conflicts                              # list pending paths
feanorfs conflicts keep <PATH> --local|--cloud|--both
feanorfs conflicts keep <PATH> --file <RECONCILED>
feanorfs conflicts show <PATH> [--open]

Version files use .original/.local/.cloud suffixes.

Subcommand Description
(bare) / list Pending paths blocked from sync
keep Resolve by keeping local, cloud, both, or a reconciled file
show Unified diff; --open launches editor compare
materialize Read-only cross-machine conflict triple materialization (--about <snapshot> selects the snapshot; --path restricts)

Multi-agent authority: agent integrator (dispatcher)

When several agents produce overlapping work, one authorized dispatcher orchestrator picks one temporary integrator per batch by auditable random draw and coordinates through encrypted signals. Assignment is advisory, not a security claim; the hub never selects an integrator; FeanorFS never merges file content.

feanorfs agent integrator assign --about <SNAPSHOT> \
  --candidate <AGENT>... [--require <CAP>...] [--exclude <AGENT>...] \
  [--exclude-author <AGENT>...] [--ack-timeout <DURATION>] "<TASK SUMMARY>"
feanorfs agent integrator status [<ASSIGNMENT-ID>]
feanorfs agent integrator revoke <ASSIGNMENT-ID> --reason "<SUMMARY>"
feanorfs agent integrator resume [--ack-timeout <DURATION>] [--fallback-on-blocked]
Subcommand Description
assign Filter the explicit roster (capabilities, availability, exclusions, neutral-author preference), draw 128-bit assignment id + 256-bit nonce from the OS CSPRNG, rank with domain-separated Blake3, offer the winner an ffint1 request
status Active assignment state, selected agent, fallback order, attempt audit trail, optional digest
revoke Explicitly stop an integrator and record the reason; replaces an accepted/active integrator with the next recorded candidate when one remains
resume Crash-safe observation pass: processes ffint1 replies since the persisted cursor; never re-sends a recorded request; cursor reset stops automatic mutation

Pre-acceptance timeout may advance to the next recorded candidate; after acceptance, timeout alone never activates a fallback. A second dispatcher fails closed on the workspace orchestration lock. Dispatcher state lives in ~/.feanorfs/workspaces/<opaque-id>/orchestrator/integrator-state.json (schema-versioned, atomic, no secrets). See agent-communication.md for the lifecycle, troubleshooting, and ffint1 details.

Orchestrator surfaces (hidden)

feanorfs events    # NDJSON: sync_state, folder_changed, conflict_risk, conflict_registered,
                   #        agent_message, agent_message_cursor_reset,
                   #        integrator_assigned/accepted/completed/blocked/requires_human, …
feanorfs mcp       # MCP protocol + tools (agent_*, conflicts_*, sync_status, workspace_log,
                   #        workspace_undo, integrator_assign/status/revoke/resume, conflict_materialize)

File contents never leave the machine on either surface. MCP sync_status returns counts plus actionable pending paths instead of the complete local file map, keeping routine agent checks small even in large workspaces.

agent_message wakeup records carry only message_id, from, to, kind, and about_snapshot — never the body. An orchestrator that sees one calls feanorfs --json agent inbox for the typed message. Wakeups are cursor-based and deduplicated by snapshot ID within a bounded in-process cache. A reset or overflow emits agent_message_cursor_reset with only the observed cursor and cursor_reset: true before the bounded wakeups; older wakeups may have been missed, so the orchestrator should re-read the typed inbox as needed.

Examples

First-time setup across two machines

# Machine A: private hub, sync watcher, and tray all persist at login
machine-a$ feanorfs start /path/to/project --workspace proj

# Machine A: create a single-use LAN pairing code
machine-a$ feanorfs pair

# Machine B: pair, join, and enable background sync
machine-b$ feanorfs start fnp1-... ~/projects/proj
machine-b$ cd ~/projects/proj
machine-b$ feanorfs status            # mirror is up to date

Continuous sync while working

feanorfs start          # resume + automatic background sync
vim src/lib.rs          # auto-syncs within 500ms of saving

Check what changed before syncing

feanorfs status
# Output:
#   Local changes not yet on the mirror (run 'feanorfs sync --up'):
#     [modify/add] src/lib.rs
#   Changes on other machines to download (run 'feanorfs sync --down'):
#     [download]   README.md (2.3 KB)

Synced files

FeanorFS syncs the current contents of the workspace folder — including hidden files and paths that git would ignore (.env, local config, scratch WIP). That is intentional: those files are often exactly what you want on another machine.

Always skipped: .git/, .jj/, and legacy FeanorFS metadata. FeanorFS creates no metadata or ignore file in the workspace.

Default artifact ignores: target/, node_modules/, .venv/, __pycache__/, dist/, build/, .next/, .cache/, plus editor swap files and .DS_Store. These are high-churn build/install trees — syncing them would hammer the watcher on every compile, not just slow the first sync.

.gitignore is not read. FeanorFS is not a git companion; workspaces need not be repos.

Custom rules are optional — run feanorfs ignore <pattern> for a project-specific exclusion. Rules are stored in private global state, not the project. Do not copy your entire .gitignore (that would exclude the files FeanorFS is for).

Full rationale: sync-scope.md.

Agent loop and reconciliation (orchestrators)

FeanorFS isolates files, not processes. An agent workspace is a normal worktree under the workspace's private global state — feanorfs agent run selects it without adding files to the project. It sets FEANORFS_AGENT, FEANORFS_AGENT_DIR, and the absolute shared control root in FEANORFS_WORKSPACE_ROOT on the child. Nested FeanorFS signal and MCP commands use that control root while the child remains cwd-scoped to the agent worktree. agent run does not sandbox the process — absolute-path writes can escape the agent dir (see threat-model.md).

Loop

feanorfs sync --no-watch          # optional: ensure folder is current
feanorfs agent spawn ci1
feanorfs agent run ci1 -- cargo test
feanorfs agent status ci1           # read-only preview
feanorfs agent refresh ci1          # pull cloud changes agent hasn't touched
feanorfs agent land ci1             # apply clean work; register needs-attention
feanorfs conflicts
feanorfs conflicts keep <path> --local | --cloud | --both | --file <reconciled>

Reconciliation protocol (LLM harnesses)

  1. feanorfs --json agent land <name> — read conflicts[] with original_file, local_file, cloud_file, hint.
  2. Optional: feanorfs agent land <name> --propose writes <path>.proposed via diff3 (text only; never auto-applied).
  3. Reconcile locally — human or agent writes a candidate file.
  4. Verify in a spawned agent workspace: agent spawn verify → copy candidate → agent run verify -- <tests>.
  5. Only a green run earns feanorfs conflicts keep <path> --file <candidate>.

Tiered policy: proactive ffwork1 coordination first; engine-proven last resort with a causally-behind designated agent; typed human escalation only for a closed-enum ambiguity.

Resolution history: feanorfs conflicts history --json (records method, resolver from FEANORFS_AGENT or human).

Demo script

./scripts/demo-agent-loop.sh

Exit codes

Code Meaning
0 Success
1 Application error (config not found, network failure, IO error, etc.)

Environment

The server respects RUST_LOG for controlling log verbosity. The client writes bounded logs under ~/.feanorfs, outside projects, regardless of RUST_LOG.

# Server: enable debug logging for the server crate and tower-http
RUST_LOG=feanorfs_server=debug,tower_http=debug feanorfs serve

# Server: silence everything except warnings
RUST_LOG=warn feanorfs serve

If RUST_LOG is unset, the server defaults to feanorfs_server=info,tower_http=info.

All other client configuration is stored under ~/.feanorfs; FEANORFS_HOME can override that root for isolated automation and tests.