Keeps chosen files and folders the same on several machines. Change a file on any machine and the others get the change within a second or two. You can sync single files, not only folders.
Built for macOS. It uses about 8–10 MB of RAM and no CPU when nothing changes.
You need Rust. On every machine:
cargo install --locked --force --git https://github.com/ravshansbox/rust-sync
rust-sync service install # start now and at every loginThis puts rust-sync in ~/.cargo/bin. --force makes cargo rebuild even if
this version is already installed, so the same command also upgrades or
reinstalls. To run the daemon in a terminal instead, use rust-sync daemon.
Upgrade or reinstall: run the cargo install command again, then
rust-sync service restart to run the new binary.
Uninstall:
rust-sync service uninstall
cargo uninstall rust-sync
rm -r ~/.rust-sync # optional: node key, config and indexOn machine A:
rust-sync path add ~/notes # a folder
rust-sync path add ~/.zshrc # or a single file
rust-sync node add macmini # shows B's ID and asks you to confirmOn machine B:
rust-sync node add macbook # B must trust A tooUse a hostname, Tailscale name or IP address. See macOS Local Network privacy for why Tailscale names are the safest choice on macOS.
That's it. B now syncs ~/notes and ~/.zshrc. The list of synced paths is
shared, so rust-sync path add on any machine applies everywhere.
To add a third machine C, pair it with any one machine (both directions). The others learn about C from that machine and connect to it by themselves.
| Command | Alias | What it does |
|---|---|---|
rust-sync daemon [--port N] [--log-file <path>] |
Run the sync process (default port 21987) | |
rust-sync path add <path> |
Start syncing a file or folder | |
rust-sync path list |
path ls |
Synced files and folders |
rust-sync path remove <path> |
path rm |
Stop syncing it. Files stay on disk |
rust-sync node add <host[:port]> [--id ID] [-y] |
Trust a node | |
rust-sync node list |
node ls |
Trusted nodes, their addresses and connection state |
rust-sync node remove <ID or name> |
node rm |
Stop trusting a node |
rust-sync node alias add <ID or name> <host[:port]> |
Check the node's ID at another address and also dial it there | |
rust-sync node alias remove <ID or name> <host[:port]> |
node alias rm |
Stop dialling a node at that address |
rust-sync service install [--system] |
Start the daemon at login, or at boot with --system (and now) |
|
rust-sync service uninstall |
Stop it and stop starting it automatically | |
rust-sync service start |
Start the daemon now | |
rust-sync service stop |
Stop the daemon now; it starts again at the next login or boot | |
rust-sync service restart |
Stop and start it, for example after an upgrade | |
rust-sync status |
Nodes, paths, connections, pending requests | |
rust-sync id |
This node's ID |
If the daemon isn't running, path add, path remove, node add,
node remove, node alias add and node alias remove edit the config file.
The changes take effect when the daemon starts.
A node can have other addresses besides the one you added it with. For example,
to reach macmini over the local network when Tailscale is down:
rust-sync node alias add macmini macmini.localThis connects to macmini.local and checks that the node there has that ID.
From then on, the daemon looks up the main address and every alias at the same
time and keeps the first connection that works. Aliases are shared, so every node
tries them. A local address like macmini.local may need Local Network access
on macOS; see macOS Local Network privacy.
- Watching: macOS FSEvents via the
notifycrate. No polling. Changes are collected for 0.5 s (at most 3 s) before a file is read. A cheap full rescan (only checks size and mtime) runs every 15 minutes in case an event was missed. - Paths under your home folder are shared as
~/…, so they work when home folders differ between machines. - Versions: each file has a BLAKE3 hash and a version vector (a counter per node). This tells a normal update apart from two machines editing the same file at the same time.
- Conflicts: the version with the newer mtime wins (an edit always beats a
delete). The losing version is kept next to it as
name.sync-conflict-<time>-<node>.ext. - Deletions are recorded, so a machine that was offline doesn't bring a deleted file back.
- Transfers: whole files, in 32 KiB chunks, at most 8 at a time. Files are written to a temporary file, checked against the hash, then renamed into place. Mtime and permissions are copied.
- Security: each node has an X25519 key pair. Its public key is its node ID.
Connections use the Noise XX handshake (the
snowcrate), so traffic is encrypted and both sides check each other's key. A node only talks to nodes it trusts. - Connecting: the main address and aliases are looked up at the same time, and every address they resolve to is tried in parallel, with a new attempt every 250 ms (Happy Eyeballs, RFC 8305), and the first that connects is used. After a failed dial the node retries after 1 s, 2 s, 4 s… up to 5 s. It listens on IPv4 and IPv6.
- Liveness: connected nodes ping each other every 5 s and drop a connection after 15 s without a message, then dial again.
- Network changes: when an address or interface changes (for example Tailscale or Wi-Fi goes down), connections whose local address is gone are dropped and every missing node is dialled at once.
- State is kept in
~/.rust-sync/(key,config.json,index.bin). SetRUST_SYNC_HOMEto use a different folder.
rust-sync service installThis installs a per-user launchd agent
(~/Library/LaunchAgents/com.github.ravshansbox.rust-sync.plist) that runs
rust-sync daemon at login and restarts it if it crashes.
rust-sync service stoppauses syncing: the daemon stops and stays stopped untilrust-sync service startor the next login (or boot, with--system).rust-sync service restartruns the current binary, for example after an upgrade.- Run
service installagain after moving the binary, because the agent runs it from where it was when you installed. If you build from source, copy the binary somewhere stable first. rust-sync service uninstallremoves the agent and leaves your config and files alone.
Log: ~/Library/Logs/rust-sync.log. Each line starts with the time in UTC.
At 1 MiB the file is renamed to rust-sync.log.1, replacing the older one, so
the log never takes more than about 2 MiB. When you run rust-sync daemon in a
terminal, the log goes to the terminal instead, or to a file with
--log-file <path>.
A launchd agent is not exempt from Local Network privacy (below). Use Tailscale names for nodes, or allow the prompt if macOS shows one.
An agent only runs once you log in on the Mac's screen. Logging in over SSH does not start it. On a machine that restarts with nobody logging in, such as a headless Mac mini, install a launchd daemon instead:
rust-sync service install --systemThis asks for your password (through sudo) and installs
/Library/LaunchDaemons/com.github.ravshansbox.rust-sync.plist. The daemon
still runs as you, with your home folder and state, and starts at boot. It is
exempt from Local Network privacy. Installing one kind replaces the other.
service start, stop, restart and uninstall work with either, and ask for
your password when the daemon kind is installed.
Since macOS 15, a process may need permission to connect to devices on your
Wi-Fi or Ethernet network (including a Thunderbolt Bridge). When it's blocked,
connections fail with No route to host, and the daemon logs a hint.
- Exempt: processes started from Terminal or over SSH (while that session is open), root processes, and launchd daemons.
- Not exempt: launchd agents, and a daemon started over SSH once that session has closed. We measured the second case.
- VPN addresses are not "local network". Using Tailscale names (for example
rust-sync node add macmini) avoids the problem. This is the recommended setup.
Source: Apple, TN3179: Understanding local network privacy.
These are known and accepted for now. The decision records explain each one.
- It's not a backup. When a peer deletes or changes a file, rust-sync deletes or overwrites your copy and keeps nothing. A mistake on one machine, such as deleting a folder, reaches every machine within seconds. Keep a separate backup, for example Time Machine. (ADR 0002)
- Trust is transitive. A trusted node can introduce new nodes and add paths to sync, including paths outside your home folder. Only pair machines you control. (ADR 0001)
- Every node must run the same version. Nodes don't tell each other which version they run. After an upgrade that changes how nodes talk, a node left on the old version stops syncing, with confusing errors in its log. Upgrade all machines together. (ADR 0003)
- No automated tests. Changes are checked by hand. (ADR 0004)
- Large files are slow. A small change to a large file sends the whole file again, and while a large file is being hashed the daemon does nothing else. (ADR 0005)
- The clock decides conflicts. When two machines change the same file at the same time, the change with the later file time wins, so a machine with a wrong clock can win when it shouldn't. The other version is still kept as a conflict copy.
- macOS only. It does not build on other systems.
- Not synced: empty folders, symlinks, extended attributes. Temporary files
(
*.rsync-tmp) and.DS_Storeare ignored. - Every node connects to every other node. This is fine for a handful of machines.
MIT. See LICENSE.