Summary
Revoking Accessibility permission while Tatami is running freezes all macOS input (mouse clicks, keyboard, cmd+tab — everything), and recovery requires a reboot. The freeze is a known issue; the AX-revoke handling work was backed out (commit revert(input): back out the AX-revoke freeze work) rather than ship a fix that didn't hold.
Symptom
- Toggle Tatami off under System Settings → Privacy & Security → Accessibility while it's running → the whole system stops responding to clicks/keys. Force-quit isn't reachable; a reboot is needed.
- Distinctive clue: after the gesture tap was moved off the main run loop, the 4-finger workspace swipe still works during the freeze while everything else is dead — i.e. one tap's events still flow but the shared session event stream is otherwise wedged.
Mechanism (diagnosis)
Tatami installs several active CGEventTaps (kCGEventTapOptionDefault) on the shared session stream (.cgSessionEventTap):
| Tap |
Events |
Always active? |
| FocusFollowsMouse |
mouseMoved |
yes (when FFM on) |
| MirrorClick |
leftMouseDown / rightMouseDown |
only when floating mirrors exist |
| BorrowChord |
keyDown |
only while a borrow direction-pick is armed |
| Gesture |
gesture |
when gestures on |
An active tap is in-line in the HID delivery path. On AX revoke the taps lose authorization; left armed (and our tapDisabled handlers re-enabled them), they wedge the shared session stream — so clicks, keys and cmd+tab all freeze even though only mouseMoved/click events route through Tatami's taps. The always-on FocusFollowsMouse tap (mouseMoved) is the prime suspect; MirrorClick wedges clicks when mirrors are up.
What was confirmed via instrumentation
- An off-main poll (cooperative pool, not the main actor) keeps running and logging during the freeze — so off-main work survives a blocked main thread.
AXIsProcessTrusted() does flip to false after a runtime revoke (heartbeat showed trusted=false).
What was tried (all failed)
performFocus AXIsProcessTrusted() guard + accessibility.changes() teardown. The teardown runs on the main actor, which is already blocked during the freeze → it never executes.
- Move the gesture tap to
EventTapThread. Swipe then survives the freeze, proving the mechanism, but clicks/keys still froze → the gesture tap wasn't the (only) wedger.
- Tear taps down on
tapDisabled when !AXIsProcessTrusted(). Didn't engage — the system apparently doesn't reliably send tapDisabled on revoke, so the handler never fires; the taps stay armed-but-untrusted and keep wedging the stream.
Likely next direction (unvalidated)
- An off-main poll (not
tapDisabled-driven) that, on AXIsProcessTrusted() going false, proactively tears down all active session taps (FFM + MirrorClick at minimum), each teardown routed through EventTapThread so it runs even when main is blocked. Off-main detection + off-main teardown both confirmed feasible; the full teardown of MirrorClick (owned by the @MainActor FloatingOverlayController) needs an off-main-safe hook.
- Alternatives to evaluate: make the observe-only taps listen-only (moves the TCC gate to Input Monitoring — the reason they're active today), and/or a periodic
CGEventTapIsEnabled/CGEventTapCreate(...) == nil probe.
Workaround
Don't revoke Accessibility while Tatami is running. Re-granting requires a relaunch anyway, so quit Tatami first, then change the permission.
References
- Apple — Quartz Event Services (active vs listen-only taps;
kCGEventTapDisabledByTimeout; the callback must return promptly or the stream stalls).
- Ghostty discussion #11390 (active
.defaultTap on the main run loop → kernel holds all input until the tap watchdog fires).
- Apple DevForums thread 735204 (
AXIsProcessTrusted() staleness after revoke; clicks blocked).
- yabai / skhd / Hammerspoon event-tap patterns (main-run-loop active taps survive only because their callbacks never block).
Summary
Revoking Accessibility permission while Tatami is running freezes all macOS input (mouse clicks, keyboard,
cmd+tab— everything), and recovery requires a reboot. The freeze is a known issue; the AX-revoke handling work was backed out (commitrevert(input): back out the AX-revoke freeze work) rather than ship a fix that didn't hold.Symptom
Mechanism (diagnosis)
Tatami installs several active
CGEventTaps (kCGEventTapOptionDefault) on the shared session stream (.cgSessionEventTap):mouseMovedleftMouseDown/rightMouseDownkeyDowngestureAn active tap is in-line in the HID delivery path. On AX revoke the taps lose authorization; left armed (and our tapDisabled handlers re-enabled them), they wedge the shared session stream — so clicks, keys and
cmd+taball freeze even though onlymouseMoved/click events route through Tatami's taps. The always-on FocusFollowsMouse tap (mouseMoved) is the prime suspect; MirrorClick wedges clicks when mirrors are up.What was confirmed via instrumentation
AXIsProcessTrusted()does flip tofalseafter a runtime revoke (heartbeat showedtrusted=false).What was tried (all failed)
performFocusAXIsProcessTrusted()guard +accessibility.changes()teardown. The teardown runs on the main actor, which is already blocked during the freeze → it never executes.EventTapThread. Swipe then survives the freeze, proving the mechanism, but clicks/keys still froze → the gesture tap wasn't the (only) wedger.tapDisabledwhen!AXIsProcessTrusted(). Didn't engage — the system apparently doesn't reliably sendtapDisabledon revoke, so the handler never fires; the taps stay armed-but-untrusted and keep wedging the stream.Likely next direction (unvalidated)
tapDisabled-driven) that, onAXIsProcessTrusted()going false, proactively tears down all active session taps (FFM + MirrorClick at minimum), each teardown routed throughEventTapThreadso it runs even when main is blocked. Off-main detection + off-main teardown both confirmed feasible; the full teardown of MirrorClick (owned by the@MainActorFloatingOverlayController) needs an off-main-safe hook.CGEventTapIsEnabled/CGEventTapCreate(...) == nilprobe.Workaround
Don't revoke Accessibility while Tatami is running. Re-granting requires a relaunch anyway, so quit Tatami first, then change the permission.
References
kCGEventTapDisabledByTimeout; the callback must return promptly or the stream stalls)..defaultTapon the main run loop → kernel holds all input until the tap watchdog fires).AXIsProcessTrusted()staleness after revoke; clicks blocked).