tether website → · Download · Report a problem
Share one keyboard and mouse across several computers on a LAN — a software KVM. No extra hardware, no cloud, and no screen streaming: only input, clipboard and file events cross the wire.
- Nothing to configure. Run the same command everywhere. The machines find each other and sort out the rest.
- Whichever keyboard you touch drives. Put a hand on the other machine and it takes over. No hotkey.
- macOS, Windows and Linux, including Wayland.
tether run --pair # on every machine, once
tether run # from then on| Install | get it onto each machine |
| First run | pairing, and what to expect |
| Arranging your screens | say where each machine sits |
| Commands | the whole CLI, one table |
| When something is wrong | the usual suspects |
| How it works | canvas, routing, suppression |
| Security | pairing and TLS |
| Platform support | what works where |
| Build from source | Rust 1.82+ |
Grab the file for your machine from the releases page.
| Download | Machine |
|---|---|
Tether-<v>.dmg |
macOS — drag to Applications |
tether-<v>-windows-x86_64.zip |
Windows — Tether.exe and tether-cli.exe |
tether-<v>-linux-x86_64.tar.gz |
Linux — tether, tether-gui, udev rule |
tether-<v>-macos-universal.tar.gz |
macOS command line only |
On macOS and Linux the one-liner works too:
curl -fsSL https://raw.githubusercontent.com/Fini240/tether/main/install.sh | shEach platform needs one thing doing once. Pick yours.
Open the .dmg, drag Tether.app to Applications, launch it, and click
⚠ Accessibility not granted. Then relaunch.
Granting it to Tether.app also covers the session it runs, because macOS
attributes the grant to the responsible process. Approving the bare CLI
instead approves whichever terminal you started it from — broader, and it comes
unstuck more easily.
"I granted Accessibility and it still says I did not" — the most confusing failure in this project
macOS ties the grant to the app's designated requirement. With an ad-hoc signature — which is what these releases use, because notarisation needs a paid Apple Developer account — that requirement is a hash of the binary itself:
$ codesign -d -r- /Applications/Tether.app
designated => cdhash H"7262c7a151084eccbbc20143f51dbc9454c1716a"
Every build changes that hash, so after an update the old grant no longer matches. The row stays in System Settings, still switched on, and macOS denies anyway — the stale entry is the problem, which is why toggling it does not help. Each update leaves another one behind.
Fix from the menu: ⚠ Accessibility not granted → "Already switched on? Reset it and re-ask". Or by hand:
tccutil reset Accessibility dev.tether.TetherTo stop it recurring, sign with a certificate rather than ad-hoc. Even a self-signed one is enough — the requirement then names the certificate instead of a hash and survives every rebuild:
packaging/macos/make-signing-cert.sh
MACOS_SIGN_IDENTITY="Tether Local Signing" packaging/macos/bundle.shThat does nothing for Gatekeeper, which wants a real Developer ID, but the permission stops falling off.
Other macOS things that are macOS, not bugs
- A downloaded binary is quarantined. Gatekeeper blames "an unidentified
developer", which sends you looking in the wrong place.
install.shclears the flag; by hand it isxattr -dr com.apple.quarantine /usr/local/bin/tether. - Injection fails silently without Accessibility — no error, no dialog. Every role checks at startup and refuses to run rather than let you conclude the network is broken.
Unzip somewhere permanent and run Tether.exe. Low-level hooks need no
permission. Windows Defender Firewall asks once — allow it on private
networks.
Two limits that are Windows' own
- A program running as administrator cannot be driven by one that is not. If everything works except an elevated app, run Tether as administrator too.
- The secure desktop — UAC prompts, Ctrl+Alt+Del, the lock screen — accepts no simulated input from anything, at any privilege level.
sudo cp 99-tether.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
sudo modprobe uinput
echo uinput | sudo tee /etc/modules-load.d/uinput.conf
sudo usermod -aG input $USERThen log out and back in — a full logout, not a screen lock. Group
membership only applies to new sessions, and this is the step people skip
before reporting that it does not work. Check with groups | grep input, then
tether doctor.
Or let the archive do all of that, and add Tether to your application launcher with its icon while it is there:
sh install.sh # binaries, icon, launcher entry, udev rule, group
sh install.sh --autostart # …and start it at login
sh install.sh --uninstall # take it back outOn Arch, Garuda, EndeavourOS there is a PKGBUILD, so pacman owns the
files and pacman -R tether takes all of them back out:
curl -O https://raw.githubusercontent.com/Fini240/tether/main/packaging/linux/PKGBUILD
curl -O https://raw.githubusercontent.com/Fini240/tether/main/packaging/linux/tether.install
makepkg -siIt installs the udev rule, because that is a packaged system file, and
deliberately does not put you in the input group — a package manager
should not hand an account the ability to read every keystroke without being
asked. It prints the one command to do it yourself.
Anyone in the
inputgroup can read every keystroke on the machine and synthesise input as any user. That is inherent to a software KVM — Synergy, Barrier and Deskflow need the same — but it is worth knowing before granting it to an account.
Why evdev rather than X11 or Wayland, and what it costs
X11 has XTEST and XInput2 and works beautifully until the machine boots a
Wayland session, which most distributions now do by default. Wayland has no
portable equivalent: capturing input globally is deliberately not something a
client may do. evdev and uinput sit below both, so one backend covers X11,
Wayland and a bare console.
Two things follow from that, and neither has a fix that would not mean giving up on Wayland:
- The cursor position is not readable. Only the display server knows it. Movement is tracked as deltas — the path every platform already uses while suppressed. Invisible on one screen; across two screens of different heights the pointer can enter the next at the wrong height.
- The screen arrangement is not readable.
/sys/class/drmgives each display's size but not where you put it. One screen is exact; several are guessed left to right and may need one drag.
One thing comes free: suppression is EVIOCGRAB, which takes the device for
this process alone, so a grabbed mouse is invisible to the display server and
the local cursor stops dead. macOS needs a whole extra mechanism for that.
tether-gui needs X11 or Wayland libraries present. The tether CLI needs
none of them and runs on a machine with no desktop at all.
Run this on every machine, once:
tether run --pairEach prints a fingerprint. Check they match, then stop them and start again
without --pair — from then on only those exact machines are accepted.
Never leave
--pairon. While it is, anything on your network can connect.
In the window it is one button: Connect.
There is no host to nominate. The machines find each other over mDNS and settle between themselves which one arbitrates the shared pointer, by comparing machine ids — lowest wins, so both ends reach the same answer without negotiating. A machine that finds nobody takes the job and picks the others up as they arrive; one that is outranked stands down, but never while somebody is connected to it.
tether host and tether client still exist for pinning the roles down by
hand, which is what you want when discovery cannot work — machines on different
subnets, or mDNS blocked.
cp packaging/macos/dev.tether.daemon.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/dev.tether.daemon.plistA LaunchAgent, not a LaunchDaemon, on purpose: a daemon runs with no window server session, where CoreGraphics reports zero displays and an event tap can never fire.
Every machine is added to the canvas automatically, to the right of everything already there. Then say where it really sits — by dragging in the window, or:
tether layout # what it thinks right now
tether layout pc left mac # the PC is to the left of the MacMACHINE ID POSITION
pc 4293ca21 -1920,0 1920x1080 Windows
mac fce7211d 0,0 1920x1080 macOS <- this machine
move left to reach pc
Machines are addressed by name, or by an id prefix when two share one. Placement is flush by design: a gap would be canvas belonging to no monitor, and the pointer would stop dead in it instead of crossing.
The CLI writes the config file, and a running session only reads the layout when it starts — restart it for the change to take. The window applies it immediately.
tether run |
join the network; the normal way to start |
tether run --pair |
…and trust a new machine, once |
tether doctor |
check permissions, capture, injection and the handoff guard |
tether layout |
show the screen arrangement |
tether layout <a> left <b> |
place a relative to b — also right, above, below |
tether discover |
machines advertising on this network |
tether id |
this machine's identity and pairing fingerprint |
tether screens |
this machine's displays, as the layout engine sees them |
tether name <new> |
rename this machine |
tether config |
config file path and contents |
tether host / tether client |
pin the role by hand |
Start here. It checks everything that fails silently:
tether doctorPlatform macOS
Input permission granted
Displays 1 found
Input capture started
Injection marking working (5 of our own events filtered)
Automatic input handoff is safe to use on this machine.
The pointer will not cross. Check both machines are running, are on the
same subnet, and that the arrangement actually puts them next to each other on
the edge you are pushing against — tether layout.
Control keeps snapping back to the other machine. That is
auto_input_handoff working as designed: touching a machine's own keyboard or
mouse gives it control. Set auto_input_handoff: false in the config if it
gets in the way. cursor_follows_input controls whether the pointer jumps to
the machine you touch or stays put and is driven from afar.
It says injection marking is BROKEN. Either the machine cannot tell its own injected input from a hand on the keyboard — in which case two machines will fight over which is driving — or you touched the mouse during the two seconds the test runs. Run it again without touching anything.
Everything looks right and nothing happens. Turn the log up; it names the machine that claimed control and the event that did it:
tether run --log debugEvery monitor of every machine is placed on one virtual canvas. Whichever machine is arbitrating keeps a single authoritative cursor position on it and moves that by the deltas it is given. Whichever monitor contains the resulting point decides who receives the event — so crossing a screen edge is a lookup, and a machine with three displays is simply three rectangles. Nobody else does layout maths; they are handed absolute coordinates in their own space.
Input flows in both directions regardless of which machine arbitrates. That is why the role can be elected rather than configured, and why it is invisible.
While the pointer is elsewhere, a machine suppresses its own input so keystrokes do not land on two machines at once, and pins its own cursor so it stays where it was left. Those are two different jobs: suppressing stops applications from seeing the movement, but the cursor sprite is drawn from the input stream underneath that, so without pinning the arrow goes on gliding around the machine you walked away from — and turns up somewhere else when you come back.
| suppress | pin | |
|---|---|---|
| macOS | CGEventTap returning NULL |
CGAssociateMouseAndMouseCursorPosition(false) |
| Windows | hook returns non-zero | re-centre against an anchor |
| Linux | EVIOCGRAB |
the same grab |
Keys travel as USB HID usage codes — physical key positions, not characters — so the receiving machine's own keyboard layout applies. Crossing between macOS and anything else swaps Control and Meta, which is what makes ⌘C arrive as Ctrl+C and back again.
Telling injected input from a real hand
Without this, the moment one machine injects a mouse move into another, the receiver sees "input!" and grabs control straight back — the two ends fight forever.
macOS stamps kCGEventSourceUserData on every synthesised event and ignores
stamped events in the tap. Windows gets LLMHF_INJECTED for free. Linux needs
neither: it injects through its own uinput device nodes, which the capture
side never opens, so there is nothing to recognise and nothing that could leak.
tether doctor proves whichever applies before you rely on it.
There is no certificate authority on a LAN, so trust is fingerprint pinning,
like SSH's known_hosts:
- Each machine generates a self-signed certificate once. Its SHA-256 fingerprint is its identity.
--pairaccepts an unknown fingerprint once and records it. Both ends print it so you can compare.- Afterwards only recorded fingerprints are accepted, in both directions.
All traffic is TLS 1.2/1.3 (rustls, ring provider). Both ends authenticate —
this connection carries every keystroke you type, including passwords, so
one-sided authentication would not be enough. The pairing window is the weak
point: someone on your network during it could interpose. Compare the
fingerprints.
| capture | inject | monitors | clipboard | lock | |
|---|---|---|---|---|---|
| macOS | ✅ CGEventTap | ✅ CGEvent | ✅ | ✅ | ✅ |
| Windows | ✅ WH_*_LL hooks | ✅ SendInput | ✅ | ✅ | ✅ |
| Linux | ✅ evdev | ✅ uinput | ✅ | loginctl |
|
| headless | ✅ | ✅ | ✅ | ✅ | ✅ |
loginctl, which not every session provides. Both are
explained above.
Working: edge switching, multi-monitor, screen arrangement, automatic input handoff, cursor lock, jump-to-machine hotkeys, modifier remapping, clipboard text and images, mDNS discovery with manual fallback, TLS with pinning, config persistence, graceful reconnect, automatic arbiter election.
Not yet: file transfer (frames exist, offers are refused), rich-text clipboard (degrades to plain text), lazy clipboard pull, live re-layout when a machine's resolution changes, hotkey suppression while the pointer is away, a tray icon, run-at-login on Windows.
What is actually tested, and what is not
cargo test covers geometry, keymap, codec, TLS, discovery and the arbiter
election, plus an end-to-end test that runs two roles in one process over a
real TLS socket and asserts that the cursor crosses, coordinates translate, and
keystrokes arrive. CI runs the suite on macOS, Windows and Linux.
The native backends are not exercised by that suite — an event tap needs a logged-in graphical session, and a uinput device needs real permissions; neither is available to CI. They are verified by hand:
- macOS ⇄ Windows and macOS ⇄ Linux crossings, both directions, on real hardware.
- Linux:
tether doctoron Garuda reports capture started, displays enumerated, and nothing leaking back. - The keycode tables and DRM mode parsing do run in CI on Linux.
Not verified: a second physical Mac, and any BSD.
Rust 1.82+.
cargo build --releaseTwo machines in one terminal each, on synthetic screens, no second computer:
# terminal 1
./target/release/tether --config /tmp/a/config.json --backend headless \
host --pair --port 24800
# terminal 2
./target/release/tether --config /tmp/b/config.json --backend headless \
client --pair --host 127.0.0.1:24800Release artifacts:
packaging/macos/bundle.sh # Tether.app, .dmg, signed CLI, checksums
packaging/windows/cross-build.sh # Windows .exe files, from a Mac or Linux| crate | what lives there |
|---|---|
tether-proto |
wire types and framing — everything that crosses the network |
tether-core |
canvas, cursor router, keymap, hotkeys, config. No OS, no network |
tether-platform |
the OS boundary: capture, injection, monitors, clipboard |
tether-net |
TLS with pinning, identity, mDNS |
tether-daemon |
the tether binary: arbiter election and both session loops |
tether-gui |
the window: arrangement canvas, controls, status. egui |
The interesting logic is in tether-core::transition, and it is entirely
unit-testable without a second computer, which is the point of keeping it
there.
MIT OR Apache-2.0.
