fix: make the event monitor self-healing and surface Secure Input - #5
Merged
gentlespoon merged 7 commits intoJun 12, 2026
Merged
Conversation
macOS disables a CGEventTap whose callback exceeds its time budget and delivers tapDisabledByTimeout/tapDisabledByUserInput as pseudo-events. These fell into the default switch case, so the tap stayed dead until a manual restart via the menu. Re-enable it right in the callback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Watchdog (5 s) re-enables a disabled tap, tears down an invalidated mach port, and starts the monitor once accessibility trust exists. This also covers: permission granted after launch (login autostart), silent tapCreate failure, and trust loss after re-signing. - isMonitoring now reports the actual tap state (tapIsEnabled) instead of mere existence of the mach port. - tapCreate failure is logged instead of claiming the monitor started. - Trust-loss restart no longer runs inside the tap callback (it would invalidate the mach port whose callback is executing); it is dispatched async to the main queue instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AX calls to an unresponsive app block for up to ~6 s by default. The tap callback does such calls synchronously, so one beachballing app could exceed the tap's time budget and get it disabled by macOS. 250 ms is generous for healthy apps; window placement on slow apps is still covered by the existing verify-and-retry loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop mouseMoved and keyUp from the event mask: neither is handled, yet every such event in the system was routed through the active tap. - Stop polling the AX window position on every leftMouseDragged event (hundreds of sync IPC round-trips per second to the very app that is busy handling the drag). startSnapping() already recomputes the drag state on demand, so behavior is unchanged. Together with the AX timeout cap this removes the main trigger for tapDisabledByTimeout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frontMostWindow can legitimately be nil (click on the desktop, AX lookup failure), but startSnapping still set isSnapping = true and the subsequent drag/up handlers force-unwrapped it — crashing the app. startSnapping now aborts cleanly when no window is resolved, and the drag/up handlers guard instead of unwrapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While any app holds Secure Event Input (browser password fields, Terminal's Secure Keyboard Entry, password managers), the WindowServer withholds keyboard events from event taps while mouse events keep flowing — right-click activation works but the activation key appears dead. The watchdog now detects the state, logs which app holds it, and the status bar icon switches with an explanatory tooltip while it is active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- startPollingWindow/stopPollingWindow, dragCheckTimer and windowCoordinatesEnd: the polling was never started and the value never read. - isSpacePressed: never read. - CGSDisableUpdate/CGSReenableUpdate private-API declarations: never called. - Commented-out DockPosition enum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gentlespoon
approved these changes
Jun 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi @gentlespoon,
after reproducing this a bunch of times I let @claude finally have a look at it. Currently testing it, but seems to work fine. Feel free to also have a look and refine if you have any other ideas on why the frequent event monitor restarts have been necessary since my last PR.
Best regards,
@lukas-runge
Problem
The event monitor regularly died and needed a manual restart via the menu. Separately, sometimes right-click activation kept working while the activation key (Space) appeared dead.
Root causes
1. Tap disabled by macOS, never re-enabled. The tap is an active filter, so every matched event waits on our callback. The callback did synchronous AX IPC on every click system-wide and on every
leftMouseDraggedevent (hundreds/sec), with the default AX messaging timeout of ~6 s per call to a busy app. Once a callback exceeded the tap's time budget, macOS senttapDisabledByTimeout— which fell into thedefault:switch case, so the tap stayed dead until a manual restart.2. Secure Event Input. While any app holds Secure Input (browser password fields, Terminal's Secure Keyboard Entry, password managers), the WindowServer withholds keyboard events from event taps while mouse events keep flowing — exactly the "right-click works, Space doesn't" asymmetry. Not a code bug, but the app gave no indication of it.
Changes (one commit each, in order)
tapDisabledByTimeout/tapDisabledByUserInputarrives in the callback.tapCreatefailure, trust loss after re-signing).isMonitoringnow reports the real tap state; trust-loss restart no longer runs inside the tap callback.mouseMoved/keyUpfrom the event mask; stop polling the AX window position on every drag event (the drag state is recomputed on demand instartSnapping(), so behavior is unchanged).frontMostWindowcan legitimately be nil and crashed the app before.isSpacePressed, unused private CGS API declarations).Testing
Manually tested on macOS 15: snapping via Space and right-click (with and without
moveOnActivate), Escape cancel, multi-monitor drags, and the Secure Input flow (Terminal → Secure Keyboard Entry → status bar indicator appears within ~5 s, Space blocked while right-click keeps working, everything reverts on release).🤖 Generated with Claude Code