Lock every Synergy-client Mac when the Synergy server Mac locks its screen.
When you lock your primary Mac (via Ctrl+Cmd+Q, idle timeout, or anything
else), a LaunchAgent fires ssh <host> pmset displaysleepnow at each host
in your synergy.conf. One-way, lock-only — no unlock, no remote wake, no
password handling.
- macOS on the server (uses
notifyutil,pmset, and LaunchAgent — no Linux support). - Synergy running as a server on this Mac,
with a
~/Library/Preferences/Synergy/synergy.conflisting your clients. - Passwordless SSH from the server to every client (
ssh <client> truemust succeed without a prompt). Set up keys withssh-copy-idif needed. - mDNS resolution working — the default targets client screens as
<short-name>.local.
git clone git@github.com:smartwatermelon/lock-sync.git ~/Developer/lock-sync
cd ~/Developer/lock-sync
bin/installbin/install is idempotent. It:
- Symlinks
bin/lock-watcher,bin/lock-fanout, andbin/list-clientsinto~/.local/bin/. - Writes the LaunchAgent plist to
~/Library/LaunchAgents/com.smartwatermelon.lock-sync.plist. - Loads the agent via
launchctl bootstrap gui/$UID.
The agent logs to ~/Library/Logs/lock-sync.log.
By default every client is targeted as $USER@<host>.local. If some hosts
need a different SSH user, create ~/.config/lock-sync/config (overridable
via LOCK_SYNC_CONFIG):
# ~/.config/lock-sync/config
# whitespace-separated pairs, `#` starts a line comment
tilsit.local operator
mimolette.local admin
Unlisted hosts fall back to $USER. Absent or empty file is a valid state.
By default, lock-guard (running on each client) suppresses the lock if it
detects zoom.us, Microsoft Teams, or FaceTime running. To customize
this list, create ~/.config/lock-sync/guard-processes (overridable via
LOCK_SYNC_GUARD_PROCESSES):
# ~/.config/lock-sync/guard-processes
# one process name per line, `#` starts a line comment
zoom.us
Microsoft Teams
FaceTime
Slack
Absent file uses the built-in default. Present file replaces it entirely (not merged).
lock-guard's Google Meet detection requires macOS Automation permission.
Clients don't need the repo cloned or bin/install run on them — lock-fanout
pushes lock-guard to each client automatically over SSH the first time it
locks that client (see "How clients get lock-guard" below).
Expect a permission dialog on the client's screen the first time it locks
after being provisioned. macOS shows the Automation permission prompt on
the client machine itself — even though lock-guard is invoked
non-interactively over SSH — the first time it tries to talk to Chrome. This
happens automatically on every client, not just the one you set up manually.
Until someone is at that client to click Allow, Meet-tab detection silently
skips there (the process-list and microphone checks still work; see below).
After that's happened once, grant the Automation permission by running the provisioned copy interactively at a Terminal on the client:
~/.local/bin/lock-guardWhen prompted, navigate to System Settings > Privacy & Security > Automation and allow Terminal (or your shell's name) to automate Chrome. Without this permission, Meet-tab detection silently skips (the process-list and microphone checks still work).
Clients are zero-footprint: no clone, no bin/install, no manual copying.
Each time lock-fanout runs, it checksums the local bin/lock-guard
against the copy at ~/.local/bin/lock-guard on the client (shasum -a 256 over SSH) and pushes a fresh copy only if it's missing or stale,
creating ~/.local/bin on the client if needed. If that provisioning step
fails for a client (unreachable, missing shasum, etc.), lock-fanout
logs warn=provision-failed reason=<missing-local-lock-guard|ssh-failed>
(with ssh_exit=<rc> appended for the ssh-failed case) for that client
and falls back to a bare pmset displaysleepnow call for that cycle — the
client still locks, just without meeting-suppression until the client is
reachable again. The controller itself is always skipped in fan-out
(warn=skip-self), since it appears in its own Synergy client list but an
SSH round-trip to itself would achieve nothing a local lock doesn't.
After install, lock the screen (Ctrl+Cmd+Q) and tail the log:
tail -f ~/Library/Logs/lock-sync.logExpected output for each lock event:
2026-04-17T21:20:25Z locked
2026-04-17T21:20:26Z client=asiago.local user=andrewrich ssh_exit=0
2026-04-17T21:20:26Z client=tilsit.local user=operator ssh_exit=0
2026-04-17T21:20:26Z client=mimolette.local user=andrewrich ssh_exit=0
ssh_exit=0 means the client accepted the lock. A non-zero value flags a
per-host problem (ssh keys, network, or mDNS name) that doesn't block the
rest of the fan-out.
cd ~/Developer/lock-sync
git pull
launchctl kickstart -k "gui/$(id -u)/com.smartwatermelon.lock-sync"The symlinks in ~/.local/bin/ point at the repo, so git pull is
live. kickstart -k restarts the running agent so it picks up the new
binary.
cd ~/Developer/lock-sync
bin/uninstallIdempotent. Reverses install; preserves ~/Library/Logs/lock-sync.log so
you can inspect history.
Nothing in the log after locking. Check the agent is running:
launchctl print "gui/$(id -u)/com.smartwatermelon.lock-sync" | head -5state = running and a recent pid are what you want. If not running,
check ~/Library/Logs/lock-sync.log and the agent's spawn errors with
launchctl print full output.
ssh_exit=255 on one client. That's the ssh "connection failed"
exit. Test manually:
/usr/bin/ssh -o BatchMode=yes <client>.local trueFix the key or DNS issue until that succeeds, then re-lock the screen to retry. No agent restart needed.
error=list-clients in the log. Something's wrong with the Synergy
conf parser. Run it directly:
bin/list-clientsThat should emit one hostname per line. If not, check
~/Library/Preferences/Synergy/synergy.conf exists and has a
section: screens block.
bin/
├── install # set up symlinks + plist, bootstrap into launchd
├── uninstall # reverse install, preserve log
├── list-clients # parse synergy.conf → <short>.local hostnames
├── lock-watcher # subscribe to Darwin screen-lock notification
├── lock-fanout # provision + ssh lock-guard to each client
└── lock-guard # client-side lock suppression (meeting detection)
tests/ # bats test suite (`bats tests/` to run)
docs/plans/ # per-slice design docs
MIT — see LICENSE.md.