Encrypted clipboard sync for a mesh of Wayland machines on a LAN. Copy on one host, paste on all of them.
Note: This project is entirely LLM/Claude-coded — I haven't hand-written any of the code. That said, I run it daily across my own machines.
- Full peer mesh over TCP, every node dials every other; duplicate
connections are tolerated by design (node-ID based dedup). Peers are
connected point-to-point and clipmesh does not forward between them, so
every node must list every other node directly — leaving a node out of
another's
peersmeans copies won't reach it, even via a shared third node. - Noise NNpsk0 encryption keyed by a preshared secret: peers without the secret can neither read nor inject clipboard contents.
- Mirrors clipboard MIME representations (text, images, ...), capped at 32 MiB
total. Which types sync is controlled per-type by a rules file, deny-by-default
(see Configuration). By default the rules file is shared across the mesh (whole-file
last-writer-wins); disable with
share_mime_rules = false. - Skips password-manager-flagged contents by default.
- Optionally links the two local selections on a host via a
[link_selections]table with one boolean per direction:clipboard_to_selection(a Ctrl+C copy also lands in the middle-click selection) andselection_to_clipboard(a mouse highlight also lands in the Ctrl+C clipboard), each off unless set. This is local-only coupling, separate fromsync_selection(which syncs each selection across the mesh). Noteselection_to_clipboardmeans selecting any text overwrites your clipboard — and, since the clipboard is always synced when this node sends to the mesh, that selection lands on every peer's clipboard too (regardless ofsync_selection). - Resyncs on reconnect: content copied while a peer was offline is pushed
to it when it comes back, newer content wins (ties broken deterministically
by node ID; disable with
resync_on_connect = false).
- A Wayland compositor implementing ext-data-control-v1 or
zwlr-data-control-v1 (niri, Sway, Hyprland, KDE Plasma; not GNOME).
Change watching and read/write are all in-process over that protocol, so
no external
wl-clipboard/wl-pastebinary is required.
On each host:
./install.sh
The script builds and installs the binary, the systemd user unit, a config skeleton, a starter MIME-rules file, and generates a secret on first run (existing config/rules/psk are never touched). Re-run it after every update; it restarts the service for you.
Then, first time only:
$EDITOR ~/.config/clipmesh/config.toml # set listen + peers
# copy ~/.config/clipmesh/psk to every other host
systemctl --user enable --now clipmesh
Manual setup (what the script does)
cargo install --path .
mkdir -p ~/.config/clipmesh
cp examples/config.toml ~/.config/clipmesh/config.toml
openssl rand -hex 32 > ~/.config/clipmesh/psk
chmod 600 ~/.config/clipmesh/psk
$EDITOR ~/.config/clipmesh/config.toml # set listen + peers
Distribute the same psk file to every host, then:
cp clipmesh.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now clipmesh
clipmesh --paste prints a node's current clipboard to stdout — a drop-in
wl-paste for a host with no Wayland compositor (a server, a container, an
SSH session, a script). It connects to a node over the same encrypted protocol,
takes the clipboard the node already pushes on connect, and writes it out:
clipmesh --paste # best text type (else first offered)
clipmesh --paste -t image/png > x.png # a specific MIME type
clipmesh --paste -l # list the offered types
clipmesh --paste -p # the middle-click selection
clipmesh --paste --node desktop:48100 # one specific node
Flags mirror wl-paste: -t/--type, -l/--list-types, -n/--no-newline,
-p/--primary. The PSK comes from the usual config (--config <path> to
override), so the host still needs a config.toml with the psk and at least one
peer — but no compositor.
By default (no --node) it tries every configured peer concurrently and
uses the first that responds, so a headless host needn't know which of its
desktops is up. Pass --node <host[:port]> to target one specific node instead.
Symlink the binary as wl-paste to make it a true drop-in on PATH (it detects
being invoked under that name):
ln -s "$(command -v clipmesh)" ~/.local/bin/wl-paste
Two caveats, both inherent to pulling a clipboard the node already pushes:
- The target must run with
resync_on_connecton (the default) and not bedirection = "receive_only", or there is nothing to push and the paste times out. -p/--primaryonly works against a node that hassync_selection = true.
This is a one-shot read: there is no wl-paste --watch and no wl-copy.
See examples/config.toml for all options and defaults.
clipmesh --sync-config rewrites your config.toml so it lists every option
with its documentation: options you've set stay active with your values, and
any option you haven't set is added as a commented default. Run it after
upgrading to discover new options. It only normalizes a config that already
loads — a config that doesn't parse is left untouched.
Note: if the daemon is running while you do this, the config-file change makes it restart (to re-read the config). The restart is harmless — only comments and commented-out defaults changed, so the effective configuration is identical.
clipmesh watches config.toml and restarts itself when it changes (most
settings — listen, peers, psk, ... — can't be applied live), so editing the
config takes effect automatically, no manual systemctl restart needed. A
change that doesn't parse is logged and ignored, leaving the running daemon
untouched.
Restarting means exiting cleanly and relying on the supervisor to start a fresh
process, so this needs the bundled systemd unit (Restart=always) or any
supervisor that restarts on exit. Run outside a supervisor (e.g. in the
foreground) and a config change just stops the daemon. Live MIME-rule reloads
(below) happen in place and don't need a supervisor.
Which clipboard types sync is decided per-type by a TOML rules file kept next to
the config (default ~/.config/clipmesh/mimetypes; see examples/mimetypes).
Each entry under [rules] is:
"<mime>" = "allow" | "deny" # or, with a per-type size cap:
"<mime>" = { rule = "allow", max = "4MiB" }
The MIME is a quoted TOML key, so types with spaces, ;, = or other
punctuation (e.g. Java dataflavors) work. The optional max caps that one type,
on top of the global max_payload_size.
A key may also be a glob: * matches any run of characters and ? matches
exactly one, so "JAVA_DATATRANSFER*" = "deny" covers a whole family at once and
"*;charset=utf-16*" = "deny" covers every UTF-16 variant. * and ? are
always wildcards (there's no escape, so a key can't match a literal */? — MIME
types never contain them). Matching is case-insensitive (ASCII). When more
than one key matches a type the most specific
wins — an exact key beats a glob, and among globs the one with more literal
(non-wildcard) characters wins (ties break toward deny). So a broad
"*;charset=utf-16*" = "deny" alongside an exact
"text/uri-list;charset=utf-8" = "allow" denies the UTF-16 variants while still
allowing UTF-8. A type already covered by a matching glob is not appended again,
so one glob keeps churn-y families (e.g. per-paste Java dataflavor cookies) out
of the file.
clipmesh manages the file for you:
-
The
unknown_mimeconfig option decides what happens to a type with no rule yet —denyby default, so nothing syncs until you allow it. Set it toallowto sync everything you haven't explicitly denied. -
synthesize_text_plain(off by default) back-fillstext/plain;charset=utf-8andtext/plainfrom a legacyUTF8_STRING/STRING/TEXTatom when a copied selection offers notext/plain*rep, so Wayland-native apps can paste content copied from X11/legacy apps (STRINGis re-encoded from latin-1,TEXTis sniffed). The synthesized types pass through these rules, so withunknown_mime = "deny"you must allowtext/plain*or they're stripped. Synthesis applies to what's broadcast to peers; to make it paste on the origin host too, also enabletake_ownership. -
take_ownership(off by default) makes clipmesh re-offer each selection after a local copy so it owns it — the clipboard then survives the source app exiting, and (withsynthesize_text_plain) the re-offered set includes the synthesizedtext/plain, so X11-sourced content pastes on the origin host too. It engages on copies made while clipmesh is running, not on content already on the clipboard when it starts. Withexclude_sensitiveon (the default), password-manager-flagged content is not re-owned, so it can't outlive the manager clearing the clipboard (withexclude_sensitive = falseit would be). -
Any new type clipmesh sees is appended automatically with the
unknown_mimedefault — so to curate what syncs, copy a few things, then edit the generated file and flip types toallow/deny. On save the[rules]table is sorted by key and any comments placed among the entries are dropped; comments above[rules](and the managed[clipmesh]table holding the sync version) are kept. -
The file is watched and reloaded as soon as it changes, so edits take effect right away — no restart needed. An entry with an invalid value is ignored (with a warning) but kept in the file rather than dropped.
-
You can manage rules from the command line instead of opening the file:
clipmesh --allow "<glob>" # add an allow rule clipmesh --deny "<glob>" # add a deny rule clipmesh --rules # list the rules and flag overlapsAn
--allow/--denywrites the rule (a literal type or a glob) and exits; a running daemon picks the change up through its watcher (and reshares it ifshare_mime_rulesis on). Any existing entries the new glob now covers are removed and printed back so you can re-add the ones you want to keep as exceptions.--rulesis read-only: it prints the rules and, for each, any other glob that also matches its key — marking redundant duplicates and precedence conflicts. Pass--config <path>to target a non-default config. -
With
share_mime_rules(on by default), the rules file is kept in sync across the mesh: edit it on one host and the others converge to it. It is whole-file last-writer-wins — the most recently edited file wins outright and replaces the others rather than merging per-type, so a type one host had curated but another never saw is dropped when the older file loses (it reappears, deny-by-default, the next time that type is copied). clipmesh stamps the file with a managed[clipmesh]table (holdingversionandorigin) to order edits; every sharing host gains that table on first connect. A peer that flips a type toallowwill make it sync on your host — that is the point. The password-managerexclude_sensitivefilter is never shared and stays local. Setshare_mime_rules = falseto keep each host's rules independent. -
synthesize_text_plain(off by default) helps content copied from X11/legacy apps. When a copied selection offers only aUTF8_STRING,STRING, orTEXTatom and notext/plain*type, clipmesh derivestext/plain;charset=utf-8andtext/plainfrom it — re-encoded to UTF-8 (STRINGis latin-1;TEXTis sniffed) and trimmed of a trailing NUL/newline — so Wayland-native apps that only understandtext/plaincan paste it. The synthesized types pass through the rules above, so under deny-by-default you must allowtext/plain*or they are stripped.