Fixes Jump Desktop's phantom-Cmd keys and client-IME conflicts when the remote Mac uses a CJK input method (vChewing, Zhuyin, Korean, Japanese, …).
Install it on the remote Mac — the one running Jump Desktop Connect.
1. Phantom Cmd keys. A long-standing Jump Desktop (Fluid protocol) bug: when the remote Mac's input source is a CJK IME, plain letter keys sent by the client arrive wrapped in synthetic Cmd-down/up events — pressing f triggers Cmd+F. Reported for Korean on the official forum years ago, acknowledged, never fixed. Karabiner-Elements cannot help: it intercepts physical HID events, while these are synthetic events injected with CGEventPost. IMEGuard uses a session-level CGEventTap, which does see them.
2. Client-IME overdeletes. Typing Chinese with the iPad/iPhone's own keyboard makes the client edit the remote screen with machine-gun bursts of backspaces plus replacement text, assuming one keystroke produced one character. With a remote IME composing (compressing several keystrokes into one character), that assumption fails and the bursts eat text you typed by other means — including newlines, teleporting the cursor to earlier lines.
Three cooperating protections, none of which touch physical-keyboard events or non-IME input sources:
-
Phantom-Cmd strip. Events injected by Jump (identified by source PID) that carry a Cmd flag arriving within 100 ms of a Jump-injected Cmd-down get the flag removed. Measured on real traffic: phantom gaps are 5.7–38.5 ms; a genuine Cmd+C is ~835 ms — over 20× apart, so real shortcuts are never affected.
-
Auto-switch (the core fix for client-IME typing). When Jump injects events carrying non-ASCII text — the signature of a client-side IME — IMEGuard switches the remote input source to your plain keyboard layout (ABC/US/Canadian/…), because that is the only configuration where the client's one-keystroke-one-character model is true. Any physical-keyboard key switches back. The first word of a session may glitch slightly (the switch can only happen after the first client-IME event is seen); everything after is exact.
-
Backspace-burst guard (safety net). Jump-injected backspaces are held for 70 ms; three or more keyDowns within 60 ms (impossible for a human or even key-repeat) mark a synthetic burst. Deletes pass through up to a ledger of everything Jump itself has typed since the last cursor-moving action (clicks, Enter, arrows, physical keys); only the excess — the part that would eat text the client didn't produce — is swallowed. Genuine backspaces pass unchanged with ~70 ms extra latency.
imeguardbar is a separate SwiftUI MenuBarExtra app (no Dock icon) that only does configuration and launchctl control — the daemon remains the only process needing Accessibility. From the keyboard icon you can:
- see status (running / waiting for Accessibility / stopped / paused), the current input source, and the last action taken
- pause all protections at once, or toggle auto-switch and the backspace guard individually
- pick the phantom-Cmd threshold (50/100/150 ms)
- restart the daemon, open or copy the log
Quitting the menu bar tool never stops the daemon; bring the icon back with
launchctl kickstart "gui/$(id -u)/com.imeguard.bar" or by logging in again.
Requires macOS 13+ and Xcode command line tools.
git clone <this repo> && cd IMEGuard
./install.shThen grant permission once: System Settings → Privacy & Security → Accessibility → press +, add ~/.local/bin/imeguard (Cmd+Shift+G to type the path), enable the toggle. The daemon starts filtering within seconds — no reboot.
Uninstall with ./uninstall.sh.
TCC ties the grant to the binary's code hash, which changes on every rebuild of an unsigned binary — you would have to remove and re-add the entry each time (toggling is not enough). Create a local self-signed identity once and install.sh signs automatically, making the grant permanent:
# 1. Certificate + p12 (-legacy is required; Keychain cannot read the modern format)
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes \
-subj "/CN=IMEGuard Signing" \
-addext "keyUsage=digitalSignature" -addext "extendedKeyUsage=codeSigning" \
-addext "basicConstraints=critical,CA:false"
openssl pkcs12 -export -legacy -out img.p12 -inkey key.pem -in cert.pem -passout pass:tmp
# 2. Import and trust (may prompt for your login password)
security import img.p12 -k ~/Library/Keychains/login.keychain-db -P tmp -T /usr/bin/codesign
security add-trusted-cert -r trustRoot -p codeSign -k ~/Library/Keychains/login.keychain-db cert.pem
rm key.pem img.p12The certificate lives only in your keychain — it is not part of the repo and does not affect anyone else. After the first signed install, re-add the Accessibility entry once more (the binary changed); it then survives all future rebuilds. When the keychain prompts that codesign wants to use the key, choose Always Allow.
imeguard --log # diagnostic: log every event, modify nothing
imeguard --verbose # filter and log
imeguard --threshold-ms 50 # phantom window (default 100)
imeguard --no-auto-abc # disable input-source auto-switching
imeguard --no-backspace-guard # disable the burst guard
Marker files under ~/.config/imeguard/ take effect within ~2 s, no restart:
touch ~/.config/imeguard/paused # pause ALL protections
rm ~/.config/imeguard/paused # resume
touch ~/.config/imeguard/backspace-guard-off # disable just the burst guard
rm ~/.config/imeguard/backspace-guard-off # re-enableTo change the daemon's flags permanently, edit ~/Library/LaunchAgents/com.imeguard.daemon.plist and re-run ./install.sh — or just use the menu bar app.
In normal (LaunchAgent) operation the log at ~/Library/Logs/imeguard.log records actions only — startup/permission state, each phantom-Cmd strip (keycode + timing), burst decisions, input-source switches, marker-file toggles. It never records what you type. Only the manual --log / --verbose diagnostic modes log per-keystroke details (keycodes and IME text payloads); the LaunchAgent does not use them.
The daemon rotates the log at startup and every 6 hours: above 5 MB the newest 512 KB moves to imeguard.log.old and the file is truncated, capping both files at ~5.5 MB combined.
The timing rules were calibrated on real traffic; verify them on yours:
swift build && .build/debug/imeguard --log(Run from a terminal, the terminal app needs the Accessibility grant.) Connect from your client, switch the remote to your IME, press f, and check three things: the phantom events' srcPID maps to a Jump process, the Cmd-down→keyDown gap fits the threshold, and the ime= field shows your IME's ID when active.
- No effect at all — confirm
~/.local/bin/imeguardis in the Accessibility list with the toggle on; check whether the log ends with "waiting for Accessibility permission". - Permission looks granted but the daemon still waits — after any change to the binary you must remove and re-add the entry (toggling is not enough), then restart the daemon (menu bar → Restart, or
launchctl kickstart -k "gui/$(id -u)/com.imeguard.daemon"). - Phantom events show
srcPID=0or a non-Jump name — your Jump version injects differently; capture a--logsession and adjust the source detection (seeEventTap.swift).
This is a personal workaround for a third-party bug, shared in the hope it helps others hitting the same wall — Korean and Japanese IME users included. It is not affiliated with Jump Desktop. Test coverage is one person's setup (macOS 26, vChewing, iPad/iPhone/MacBook clients); other combinations may need the diagnostic mode and small tweaks. An event tap that modifies keyboard events is inherently invasive: read the source (it is small), and use the pause toggle or ./uninstall.sh the moment anything feels wrong. Issues and PRs with --log captures attached are welcome, but there is no support commitment. If Jump Desktop ever fixes the underlying bug, celebrate and uninstall this.
- Keep the remote on ABC and switch to your IME only while typing CJK.
- From iPad/iPhone, type with the client's own CJK keyboard and keep the remote on ABC — composed text arrives as Unicode and bypasses the remote IME entirely (this is what auto-switch automates).
- Keep Jump Desktop updated, and report the bug on the official forum — upstream fixes beat workarounds.