Warning
lilsync is experimental! There may be bugs that could DELETE or EXPOSE
your files. I use it, but use it at your own risk.
lilsync syncs a folder between a small, trusted group of nodes.
-
Designed for peer-to-peer sync between your own machines or other trusted peers.
-
End-to-end encrypted on the LAN: peers discover each other with mDNS and use Noise-encrypted TCP connections authenticated by their Ed25519 node keys.
-
LAN-only by design: there is no relay, NAT traversal, or remote discovery.
-
No central node: every node is equal. Each keeps a full copy of the synced folder and each can invite new nodes.
Note
See SECURITY.md for the full threat model and known limitations.
Download the archive for your platform from the
latest release, then install the
lilsync binary somewhere on your PATH:
tar -xzf lilsync-*.tar.gz
sudo install -m 0755 lilsync-*/lilsync /usr/local/bin/lilsyncRelease archives are named by version and target, for example
lilsync-0.3.2-aarch64-apple-darwin.tar.gz or
lilsync-0.3.2-x86_64-unknown-linux-musl.tar.gz.
On macOS, install with Homebrew:
brew install vrypan/tap/lilsyncInstall Rust 1.94.1 or newer, then build the release binary:
cargo build --releaseThe binary is target/release/lilsync. To install it:
sudo install -m 0755 target/release/lilsync /usr/local/bin/lilsyncStart the sync daemon in the background (creates a new single-node group on first run):
lilsync start /path/to/folderLogs are written to /path/to/folder/.lil/sync.log (10 MB rolling, one
backup). To stop the daemon:
lilsync stop /path/to/folderTo run in the foreground instead (useful for debugging):
lilsync watch /path/to/folderState is stored inside the synced folder under .lil/:
| File | Purpose |
|---|---|
private.key |
Node identity key |
private.key |
Node identity key |
daemon.lock |
Exclusive process lock |
daemon.pid |
PID of the running daemon (written by start, removed by stop) |
sync.log |
Daemon log file (rolling, 10 MB cap) |
peers.json |
Known peers and group membership |
invites.json |
Pending invite tokens |
entries.bin |
Persisted entry index (survives restarts) |
lamport |
Persisted Lamport clock |
gc-watermark.bin |
Persisted tombstone garbage-collection watermark |
Generate an invite on the first node:
lilsync invite /path/to/folderThis prints an 86-character base62 ticket. On the second node:
lilsync join /path/to/folder2 <ticket>join completes the handshake and then starts the daemon in the foreground.
Use --exit to join and exit instead, then run lilsync start to launch the
background daemon.
lilsync start <folder> [--name <name>] [--poll] [--interval-ms <ms>]
[--announce-interval-secs <secs>]
lilsync watch <folder> [--name <name>] [--poll] [--interval-ms <ms>]
[--announce-interval-secs <secs>] [--status]
lilsync stop <folder>
lilsync status <folder>
lilsync invite <folder> [--expire-secs <secs>]
lilsync join <folder> <ticket> [--name <name>] [--exit]
lilsync peers <folder>
lilsync remove <folder> <id>
startforks into the background; logs go to<folder>/.lil/sync.log.watchruns in the foreground; logs go to the terminal.--status(watch only) shows a live colour peer-status view instead of log lines.--namesets a human-readable label advertised via mDNS while the daemon runs.--polluses filesystem polling instead of native OS notifications.--interval-mssets the watcher debounce window (default 500 ms).--announce-interval-secssets how oftenSyncStateis broadcast (default 10 s).--expire-secssets invite lifetime (default 3600 s).--exitmakesjoinstop after writing group state instead of starting the daemon.
Create .nolil inside the synced folder to exclude paths:
files/
*.tmp
build/
!build/keep.txt
Supported syntax (gitignore-like):
- blank lines and
#comments are ignored !negates a rule*,?,**wildcards- leading
/anchors to the root - trailing
/matches directories only
When .nolil changes, lilsync rescans the folder. Newly ignored paths stop
being tracked locally; they are not deleted on remote peers.
.lil/is always excluded from sync. Temporary files for in-flight transfers (recv-*) are stored there and removed on completion or error.- File content is streamed over encrypted TCP without buffering the whole file in memory; BLAKE3 hash is verified before the temp file is renamed into place.
- Up to 8 file downloads run in parallel per reconciliation pass.
- Periodic
SyncStatebroadcasts (default every 10 s) drive repair: any node with a different root hash initiates a Merkle tree sync. Filesystem-change announcements also include a small bounded tree hint to reduce follow-up RPCs. - Tombstones (records of deleted files) are persisted across restarts and garbage-collected after all active peers report the same state root. Nodes publish GC watermarks so stale tombstones are not accepted again later.
- Some OS metadata files are always ignored:
.DS_Store,Thumbs.db,Desktop.ini,._*,.Spotlight-V100,$RECYCLE.BIN,lost+found. - Empty directories are not tracked. If the last file in a directory is deleted, the empty directory is removed on peers.
Use lilsync watch to keep the daemon in the foreground. Log verbosity is
controlled via RUST_LOG (default: info):
RUST_LOG=info lilsync watch /tmp/node-a
RUST_LOG=debug lilsync watch /tmp/node-aWhen running as a background daemon (lilsync start), logs are written to
<folder>/.lil/sync.log. RUST_LOG is still respected.