Share your clipboard and files between your machines (macOS, Linux, Windows) from a terminal UI.
Run tsync on each machine of your local network and they find each other. You approve a new machine once, after checking its short hash matches, and from then on they reconnect by themselves over an encrypted link. Paste text in tsync on one machine and click to copy it on another; drop a file in and click to download it on the other side.
The tsnet and tcrypto packages can also be used as a Go library for local network discovery, authenticated and encrypted links between peers, and clipboard/file transfer.
Clipboard sharing and the file sharing works and is tested between macOS, Windows and Linux machines, including VMs.
tsync on a Mac linked with a Windows PC, a Linux VM and a Linux server: shared clipboard text, a file offered by the Windows machine, one downloaded from the VM and one offered to all.
Get the binary from releases, or
brew install fortio/tap/tsync
or with Go
CGO_ENABLED=0 go install fortio.org/tsync@latest # installs in ~/go/bin typically
CGO_ENABLED=0 go run fortio.org/tsync@latest # to run without installing
On macOS, leave out CGO_ENABLED=0 (needs the Xcode command line tools) to get -sync-watch with FSEvents.
Use the same version on all your machines: the protocol still changes, and peers running an older one show as "(old version)".
Run tsync on each machine. The table lists the peers found, with this machine on the 🏠 line.
- Click on a peer, or press its number, to connect to it. It turns yellow, and on the other machine it blinks blue, with a bell and a "wants to connect" message.
- On the other machine, check that the hash shown is the same as on the first machine's 🏠 line, then click it (or press its number) to accept. Both turn green, then get a 🔗 once the encrypted link is up.
- Accepted peers are remembered and reconnect by themselves when either side restarts or after a network glitch.
- Paste (Cmd-V, Ctrl-Shift-V...) in tsync to send that text to all the linked peers. It shows in a box below the table. Peers connecting later get the last text too.
- Click on a box, or press
cfor the latest one received, to copy it to your clipboard. - Drag and drop a file in tsync to offer it to the linked peers (terminals paste the dropped file's path, which tsync recognizes, quoted or not). Only its name and size are sent until someone downloads it, and peers connecting later see the offer too.
- On the other machines, click the offer in the files box, or press
dfor the latest, to download it. Nothing is written before that. Files go to~/Downloads(or the current directory if there is none, or-dir) and never overwrite anything: you getname-1.ext,name-2.ext, etc. - Directories work too (drop one, or
tsync dir): it is offered as one item, and downloaded with all its files (symlinks are skipped) into a new directory,name, orname-1,name-2... if that exists. Nothing is ever merged into an existing directory. Hidden files, empty directories, permissions (except group/other write) and modification times are kept. tsync file1 dir2...starts as usual and offers these files, useful over ssh where you can't drag and drop the remote files. If tsync was already running in another window of that machine, that one exits and the new one takes over.qor Ctrl-C to quit.
The log scrolls below; use -loglevel debug for more details.
Copying uses the OSC 52 escape sequence, which the terminal has to support and allow. Ghostty, kitty, WezTerm and Windows Terminal work as is. iTerm2 needs "Applications in terminal may access clipboard" turned on, and tmux needs set -g set-clipboard on. Apple's Terminal doesn't support it.
Escape sequences and control characters are removed from the text received before it's displayed or copied.
tsync help
tsync [flags] [file...]
The useful ones:
-nameto use another name than the hostname, eg to run 2 instances on one machine.-max-ratelimits the file sending rate (per peer connection, whatever the number of files being sent), eg-max-rate 10MB/s(K, M, G are powers of 1024, case insensitive). Default is unlimited.-sync name=path(repeatable) syncs the content of that directory with the linked peers that use the samename: it's just a label pairing the directories, which can be anywhere and called anything on each machine (eg-sync docs=~/Documentson one and-sync docs=D:\work\paperson another): each side gets what the other has new or changed. Deletions are propagated too: the other side moves its copy to.tsync-old/(a file modified on one side since the last sync wins over its deletion on the other, and a peer deleting most files at once, eg its disk not mounted, is held rather than followed). A file changed on both sides since the last sync (a conflict, eg edited on a laptop while offline), or changed on one side and deleted on the other, is resolved without losing anything: the newest version keeps the name and the other one is kept next to it asname.sync-conflict-<host>-<time>.ext, synced to all machines; review and delete it (on any machine). They're listed, with anything else to look at, in~/.tsync/sync/<name>/conflicts.txt(-sync-conflicts listleaves conflicts alone and only lists them instead). As the newest version wins, tsync warns if the machines' clocks differ by more than a second. Moved or renamed files are moved on the other side too (no new transfer), hidden files are ignored unless there is a.tsyncignore, which then lists what to ignore with the usual.gitignoresyntax (*,**, trailing/,!to re-include; no nested files, escapes or[!...]). The versions replaced by sync are kept in.tsync-old/in the folder (-sync-keep, the last 3 per file by default, 0 for none); nothing removes them otherwise, clean it up yourself when you don't need them. It's meant for directories of up to some tens of thousands of files (more works, but sync a big tree as a few-syncfolders rather than one). How it works: SYNC_DETAILS.md.-headlessruns without a UI, for a server or a service (see Headless).-sync-watch(default on) watches the sync folders for changes: only the directories with changes are scanned again (and all of it every 15 minutes, in case). When it can't (eg too many directories for the Linux inotify limit) or the notifications missed changes (eg a network folder), it goes back to scanning every-sync-rescan. On macOS it uses FSEvents when built with cgo (the release and brew binaries are); without cgo it's off by default, as it then needs a file descriptor per file.-sync-rescan(default 10s) is how often the sync folders are scanned for changes when not watched.-tsync-dir(default~/.tsync) is where the identity, trusted peers and sync state (bases, conflicts list) are, eg to run several tsync with different identities on one machine.-dirfor where received files are saved. Default is~/Downloadsif it exists, else the current directory.-peers host[:port],...to also send discovery directly to these addresses when multicast doesn't work at all (some VPNs, cloud VMs...). The port defaults to 29556. Each peer is reached through the interface the OS routes it to (eg tailscale's), no need for-targetto match: with-peerstsync listens on all addresses, multicast still uses the-targetinterface.-auto-connect=falseto not reconnect automatically to accepted peers.-clip-linesfor how many lines each clipboard box shows (3 by default).-portand-mcastfor a different discovery port or multicast group, eg to keep separate groups of machines.-mcast nonedoesn't use multicast at all (for networks or hosts where it doesn't work, eg tailscale only): list the other machines in-peers(or at least have them list this one).
VPNs like Tailscale don't carry multicast, so tsync can't find the other machines by itself: tell it where they are with -peers, using their Tailscale names or 100.x addresses:
tsync -peers laptop,desktop.tailnet-name.ts.net
- It's enough for one side to list the other: the machine receiving the discovery message adds the sender and answers directly. Listing each other works too.
- Each peer is reached through the interface the OS routes it to (the Tailscale one here), no
-targetneeded. A machine can mix-peersover the VPN and multicast on its local network. - Names are resolved when tsync starts. Unlinked peers are retried with a delay growing up to 30s, so a machine that was off or restarting is found again within that.
- On a machine with no local network to speak of (a cloud VM or server that is only on the tailnet, a container...) add
-mcast noneto not use multicast at all. tsync then listens for discovery on the-port(29556 by default) like the multicast socket normally does, so keep the same-porton all of them. - The firewall (and Tailscale ACLs) must let UDP
-portand tsync's TCP link port through between the machines (Tailscale allows everything by default). - The first connection needs the usual accept and hash check on both sides, then the machines reconnect by themselves. For a server that should only sync directories, see Headless below.
tsync -headless -sync docs=/data/docs only does directory sync, with no UI (no terminal needed), logging like the other fortio tools: readable on a terminal, JSON on stderr otherwise. It never touches the clipboard (it could hold a password) and ignores the clipboard and file offers sent to it. Only peers you already accepted (listed in checked.pub) can link: pair once with the normal UI (run tsync with the same -tsync-dir, or copy ~/.tsync), then run it headless. Requests from other peers are ignored, with a warning in the log. It stops cleanly on Ctrl-C or SIGTERM.
tsync doesn't daemonize itself, use nohup or your init system. For systemd, eg /etc/systemd/system/tsync.service:
[Unit]
Description=tsync directory sync
After=network-online.target
Wants=network-online.target
[Service]
User=ubuntu
ExecStart=/usr/local/bin/tsync -headless -sync docs=/home/ubuntu/docs
Restart=always
[Install]
WantedBy=multi-user.target
Run 2 instances with different names and identities: tsync -name a and HOME=/tmp/b tsync -name b.
- A peer isn't listed: multicast has to work between the machines. If it only works one way (eg a VM with NAT/"shared" networking, seen by the host but not the reverse), tsync's direct discovery to the peers it sees takes care of it. If it doesn't work at all, use
-peers. - A peer stays yellow: the other side hasn't accepted yet, or didn't get the request. tsync resends it a few times; click again to retry later.
- Green but no 🔗: the link uses TCP on a random port, so allow tsync in your firewall (macOS asks on first run; on Windows allow private networks). Through NAT, the side that can't be reached asks the other side to connect instead.
- "(old version)" next to a peer: upgrade tsync there.
- Forgetting a peer: accepted peers are listed, one per line, in
~/.tsync/checked.pub(%USERPROFILE%\.tsync\checked.pubon Windows). Remove the line, or the file, and restart tsync. Leaveidandid.pubalone: they are this machine's identity, and a new one means a new hash that all the peers need to accept again.
- Interface: tsync first finds which interface and local address to use by checking the route to
-target(UDP 8.8.8.8:53 by default, one of Google's DNS servers; nothing is sent to it). This avoids picking the WSL virtual interface on Windows for instance. - Discovery: about every 1.5s, plus some random jitter, each instance sends its name, public key and a random id for this run to a multicast group (239.255.116.115:29556 by default), and also directly to the peers it knows but isn't linked to yet, and to
-peers(with a delay doubling up to 30s while not linked). Peers not heard from for 10s are removed. Once linked to all the peers it knows, an instance only multicasts every 20s: the links send a keepalive every 10s (a link silent for 30s is dropped) and keep their peer listed. Discovery messages also carry a protocol level and the version: peers of older versions (before 0.23) are still handled, they just keep the old chatty behavior (no keepalive, fast discovery) and show their level in the logs. A new or restarted instance announces itself right away and at the fast rate, which makes the others speed up until they are linked with it. - Identity: each machine has an Ed25519 key pair in
~/.tsync/, created on first run. TheDDD-DDDDhash comes from the public key; checking it on both sides is what makes the first connection safe (trust on first use). - Handshake: connection requests and accepts carry a new X25519 key signed with the sender's identity and tied to both run ids, so old messages can't be replayed. Both sides derive the session keys with HKDF-SHA256, one AES-256-GCM key per direction.
- Link: one side then opens a TCP connection. Every frame on it is encrypted and authenticated, with counter nonces so frames can't be replayed or reordered, and the first frame proves which peer it is.
- Clipboard: the text (1 MiB max) and the time it was copied go to all the linked peers, and the receiver keeps the newest.
- Files: the offer (path, size, modification time) goes to the linked peers. Once accepted, the sender checks the file didn't change and streams it, followed by its SHA-256. The receiver writes a temporary file, checks size and hash, then renames it to the last part of the sender's path, cleaned up: no
../, control characters, characters Windows refuses or leading dot; look-alike spaces (like the one macOS puts before AM/PM in screenshot names) become normal spaces and invisible characters (eg bidi overrides) are removed, while accents, other scripts and emojis are kept. Saved files are never executable (mode 0644) and keep their modification time. - Directory sync: each side keeps an index (path, type, mode, size, time, SHA-256) of its synced directories, exchanges it with the peers using the same label, and compares, path by path, its version, the peer's and what both had at the last sync, to tell who changed what (no clocks needed, except to pick the newest version of a conflict). See SYNC_DETAILS.md for the full story: decisions, moves, conflicts, the mass deletion guard, and what's on the wire.
docker run --network host -v ~/.tsync:/home/user/.tsync -ti fortio/tsync
but the multicast discovery doesn't seem to work, at least on Docker for Mac.
make test # unit and (local network multicast) integration tests
make lint # golangci-lint with the fortio shared config
make e2e # end to end TUI test with 2 instances in tmux: connect, paste, copy, restart, drop and download a file, then
# 3 instances syncing a directory, one restarted headless (needs tmux)
