Debugging the MX Master 4 on macOS Tahoe
I own an MX Master 4. If you've used one, you know — it's the kind of mouse that ruins every other mouse for you. The scroll wheel has infinite scroll with electromagnetic shifting. The thumb button opens a gesture ring that lets you flick between Mission Control, App Exposé, and Launchpad without ever touching the keyboard. It's heavy in the right way. It's shaped like it was designed by someone who actually uses a mouse for twelve hours a day.
And then macOS Tahoe broke it.
Not completely. The mouse still moved. Scrolling worked. But the action ring — the feature that made me buy this thing — was dead. I'd hold the gesture button, move the mouse, and nothing. No overlay. No ring. Just a cursor moving across a screen like it was 2004.
Logi Options+ showed the mouse as connected. All the settings were there. The toggle for Input Monitoring was on. Bluetooth said "Connected." Everything looked fine. Nothing was fine.
This is the story of what was actually wrong, and the rabbit hole I went down to fix it.
The Permission Lie
The first instinct was permissions. macOS is notoriously aggressive about gating input access behind TCC (Transparency, Consent, and Control) — the system that manages which apps can monitor your keyboard, track your mouse, or access Bluetooth.
Logi Options+ was showing as enabled in Input Monitoring. The toggle was on. It looked correct.
But "looks correct in System Settings" and "actually has permission" are two different things on macOS. The ground truth lives in a SQLite database:
sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
"SELECT client, auth_value FROM access WHERE service='kTCCServiceListenEvent';"The main app (com.logi.optionsplus) had auth_value=2 — granted. Fine. But Logi Options+ doesn't do the actual input monitoring itself. It delegates to a background agent: logioptionsplus_agent, which runs under the bundle ID com.logi.cp-dev-mgr.
That agent had zero Input Monitoring permission. The UI was showing the status of the wrong process.
The Ghost Daemon
While digging through the process list, I found something else: an old daemon from the pre-Options+ era — LogiMgrDaemon — was still running. Its launch agent plist (com.logitech.manager.daemon.plist) was set to KeepAlive: true and RunAtLoad: true, pointing to an app that no longer existed at /Applications/Logi Options.app.
It was crash-looping. Exit code 78 on every attempt. Restarting immediately. Burning CPU cycles into nothing, forever, on every boot since I upgraded.
launchctl bootout gui/$(id -u) /Library/LaunchAgents/com.logitech.manager.daemon.plist
sudo rm /Library/LaunchAgents/com.logitech.manager.daemon.plistTwo lines. The ghost was exorcised. The agent CPU dropped from 88% to 3%.
The Permission Matrix
Getting Input Monitoring to the agent wasn't enough. The action ring requires a specific combination of permissions across multiple bundle IDs and multiple TCC services. Here's what the full matrix looks like:
| Permission | com.logi.optionsplus | com.logi.cp-dev-mgr | com.logi.pluginservice |
|---|---|---|---|
| Accessibility | Required | Required | Required |
| Input Monitoring | Required | Required | — |
| PostEvent | Required | Required | Required |
| Bluetooth | Required | Required | — |
PostEvent (kTCCServicePostEvent) is the one nobody talks about. It's the permission to post synthetic keyboard and mouse events — the thing that actually lets Mission Control trigger when you flick the ring. Without it, the overlay shows up but nothing happens. The events die silently.
And these permissions need to exist in the right database. Bluetooth goes in the user TCC database (~/Library/Application Support/com.apple.TCC/TCC.db). Everything else goes in the system database (/Library/Application Support/com.apple.TCC/TCC.db).
After granting everything and restarting tccd:
sudo killall tccdThe overlay appeared. The ring rendered. The actions... still didn't fire.
The Service That Wouldn't Start
The action ring doesn't just need the agent. It needs a completely separate process: LogiPluginService. A Xamarin/.NET app that acts as the bridge between the gesture overlay and the actual system actions.
It wasn't running.
More precisely — it was crashing. Repeatedly. A new crash report every few seconds. I had fourteen crash reports from the same morning.
/Applications/Utilities/LogiPluginService.app/Contents/MacOS/LogiPluginService
# Output:
# Microsoft.macOS: Failed to initialize the VMOne line. The Mono virtual machine — the .NET runtime that LogiPluginService is built on — couldn't initialize on macOS 26.2 Tahoe. The binary was an arm64 Xamarin app, and something in the Mono VM's initialization path was incompatible with Tahoe's runtime changes.
The crash stack confirmed it:
0: __pthread_kill
1: pthread_kill
2: __abort
3: abort
4: xamarin_assertion_message
5: xamarin_vm_initialize
6: xamarin_main
7: mainThe VM never even gets past startup. abort() is called inside xamarin_vm_initialize. Game over before the game begins.
The Rosetta Gambit
Here's where it gets interesting.
LogiPluginService is a universal binary — it contains both x86_64 and arm64 slices. The arm64 path was broken. But what about x86_64?
arch -x86_64 /Applications/Utilities/LogiPluginService.app/Contents/MacOS/LogiPluginServiceIt launched. No crash. No abort. The Mono VM initialized fine under Rosetta. The process stayed alive, spawned its helper processes (LogiPluginServiceExt, LogiPluginServiceNative), and started communicating with the agent.
The arm64 build of Xamarin's runtime has a bug on Tahoe. The x86_64 build, running through Apple's Rosetta 2 translation layer, works perfectly. The irony of running a native arm64 machine through an x86_64 compatibility layer to fix a compatibility issue is not lost on me.
To make this permanent, I stripped the arm64 slice from the binary:
# Backup the original
sudo cp LogiPluginService LogiPluginService.bak
# Extract x86_64 only
sudo lipo LogiPluginService.bak -thin x86_64 -output LogiPluginService
# Re-sign
sudo codesign --force --deep --sign - /Applications/Utilities/LogiPluginService.appNow macOS has no choice — it runs LogiPluginService through Rosetta every time. The Mono VM initializes. The plugin service stays alive. The action ring works.
The Full Autopsy
Here's everything that was wrong, in order of discovery:
1. Zombie launch daemon. The old com.logitech.manager.daemon was crash-looping endlessly, pointing to a deleted app. KeepAlive: true meant it never stopped trying. Removed the plist, unloaded the agent.
2. Missing agent permissions. The background agent (com.logi.cp-dev-mgr) was missing Input Monitoring, Accessibility, PostEvent, and Bluetooth permissions. The main app had them, but the agent — which does the actual work — didn't.
3. Missing plugin service permissions. com.logi.pluginservice had zero TCC entries. Needed Accessibility and PostEvent to actually execute gesture actions.
4. Stale old Logi Options remnants. /Library/Application Support/Logitech.localized/ had dead directories from the pre-Options+ era — old kernel extensions, preference panes, daemon apps. All cleaned out.
5. LogiPluginService crash-looping. The Xamarin Mono VM fails to initialize in arm64 mode on macOS 26.2 Tahoe. The fix: force Rosetta by stripping the arm64 slice from the universal binary.
What I Actually Like About This Mouse
After all of that, the action ring works again. And honestly, it's worth the trouble.
The MX Master 4 is the first mouse I've used where the gesture button feels like a genuine workflow tool rather than a gimmick. Hold the button, flick up for Mission Control, flick down for App Exposé, flick left for desktop switching. It's muscle memory now. I don't think about it. My hand just knows.
The infinite scroll wheel is the other thing. That electromagnetic clutch that switches between ratcheted scrolling and free-spin — there's a physical satisfaction to it that no trackpad can match. Flicking through a long document and feeling the wheel spin freely, then clicking it back into detented mode for precise scrolling. It's a small thing. It matters.
The thumb wheel for horizontal scrolling is something I didn't know I needed until I had it. Spreadsheets. Timelines. Wide code files. It's just there, doing its job quietly.
And the ergonomic shape — I have large hands, and this is the first mouse where my palm actually rests instead of hovering. After twelve-hour coding sessions, that's the difference between a sore wrist and forgetting you're holding anything at all.
The Lesson
The permissions system on macOS is a house of mirrors. What you see in System Settings isn't necessarily what the system is enforcing. The TCC database is the source of truth, and it operates at the bundle ID level — which means a single "app" can be three or four different processes, each needing their own permissions, split across two different databases.
Logi Options+ compounds this by splitting its functionality across a main Electron app, a native background agent, and a separate Xamarin plugin service. Each one needs different permissions. None of them tell you when something is wrong in a useful way.
And when things break, they break silently. The action ring doesn't show an error. It just doesn't appear. The plugin service doesn't log a warning you'd ever see. It just crashes and stops retrying after five attempts.
The Rosetta fix is a hack. I know it's a hack. But it's the kind of hack that works right now, today, while Logitech sorts out their Xamarin runtime compatibility with Tahoe. The original binary is backed up. When they ship a fix, I'll restore it.
I thought that was the end of the story.
Update: the next day
The ring was dead again.
My first guess was the same one you'd make: Logitech shipped an update overnight and replaced LogiPluginService with a fresh universal binary, wiping the lipo change. I checked:
lipo -info /Applications/Utilities/LogiPluginService.app/Contents/MacOS/LogiPluginServiceStill non-fat x86_64. The Rosetta gambit was still in place. So the regression wasn't "they overwrote my binary."
Two other things were going wrong instead.
1. LogiPluginService wasn't running. The action ring isn't magic in the driver — it needs that Xamarin bridge process alive. Sometimes it simply isn't: sleep, a crash loop that exhausted retries, or the agent coming up before the plugin. When it's down, you get exactly yesterday's symptoms: cursor moves, scrolling works, no overlay, no ring.
2. A split verdict in TCC. In /Library/Application Support/com.apple.TCC/TCC.db, com.logi.pluginservice had Accessibility granted (auth_value=2). But there was a separate row keyed by the executable path — /Applications/Utilities/LogiPluginService.app/Contents/MacOS/LogiPluginService with client_type set for a path, not a bundle — and that row had auth_value=0. The bundle said yes; the path said no. System Settings doesn't surface that distinction clearly. The database still does.
Useful sanity query:
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
"SELECT client, service, auth_value FROM access WHERE client LIKE '%LogiPlugin%' OR client LIKE '%pluginservice%';"Aligning the path row with the bundle (Accessibility allowed for that path), then restarting the daemon that enforces policies:
sudo killall tccd…plus restarting the Logi agent (launchctl kickstart on com.logi.cp-dev-mgr) and letting LogiPluginService respawn, put the ring back.
I wrapped the "re-thin if needed, re-sign, kickstart agent, ensure the plugin process is up" flow in ~/bin/fix-logi-plugin-service.sh so I'm not improvising after every update. Direct edits to TCC.db are a last resort — the supported fix is toggling entries under System Settings → Privacy & Security → Accessibility until every Logi-related identity is on — but when the UI and enforcement disagree, SQLite is still where the truth lives.
So the sequel wasn't "the hack failed." It was the same symptom, two new causes: process not running, and path-level TCC denial shadowing bundle-level approval.
Until Logitech ships a proper arm64 plugin service on Tahoe, I'll keep the script and the query in my back pocket.
And once again — my mouse works. That's all I wanted.