macOS TCC Permission Checks for Input Monitoring and Accessibility in OpenLogi

OpenLogi requires macOS Input Monitoring permission to open HID devices via IOHIDManager and Accessibility permission to create CGEventTap mouse hooks, with runtime checks implemented in crates/openlogi-hid/src/permissions.rs and error handling in crates/openlogi-hook/src/macos.rs.

OpenLogi is an open-source HID++ device manager that interacts with low-level macOS APIs. To function correctly, it must secure two distinct TCC (Transparency, Consent, and Control) entitlements: Input Monitoring for hardware communication and Accessibility for event interception. These macOS TCC permission checks for Input Monitoring and Accessibility in OpenLogi are implemented across multiple crates to separate concerns between the GUI, agent, and hardware abstraction layers.

Input Monitoring Permission for HID Device Access

Why Input Monitoring Is Required

The Input Monitoring permission (internally referred to as "Listen Event" access) is mandatory because the HID++ transport layer opens low-level HID devices through IOHIDManager. macOS blocks every IOHIDDeviceOpen call unless the process holds the Input Monitoring consent.

Checking Permission Status

According to the OpenLogi source code, the current authorization state is queried in crates/openlogi-hid/src/permissions.rs. The has_access() function wraps IOHIDCheckAccess(IOHIDRequestType::ListenEvent) and returns a boolean.

The non-prompting status is also exposed as PermissionStatus in crates/openlogi-permissions/src/macos.rs via the input_monitoring() function.

// crates/openlogi-hid/src/permissions.rs
pub(super) fn has_access() -> bool {
    matches!(
        IOHIDCheckAccess(IOHIDRequestType::ListenEvent),
        IOHIDAccessType::Granted
    )
}

Requesting Access Programmatically

To prompt the user, OpenLogi calls IOHIDRequestAccess(IOHIDRequestType::ListenEvent) through the request_access() wrapper. This blocks the calling thread until the user responds to the modal dialog, so it must be executed off the async runtime using tokio::task::spawn_blocking.

use openlogi_hid::permissions;

if !permissions::has_access() {
    // Request dialog blocks; run in a dedicated thread
    std::thread::spawn(|| {
        permissions::request_access();
    }).join().unwrap();
}

Accessibility Permission for Event Taps

Why Accessibility Is Required

The mouse-button remapping hook relies on a CGEventTap to receive low-level mouse events. Without the Accessibility grant, CGEventTapCreate fails, and the agent cannot start the hook.

Detecting Denied Access in Code

The hook installation logic resides in crates/openlogi-hook/src/macos.rs. When CGEventTapCreate returns null, the implementation produces HookError::AccessibilityDenied (see line 989).

The current trust state can also be observed via the watcher in crates/openlogi-agent-core/src/watchers/accessibility.rs.

Triggering the System Prompt

Unlike Input Monitoring, the Accessibility prompt is triggered via inter-process communication. The desktop GUI sends Command::RequestAccessibilityPrompt (defined in crates/openlogi-desktop/src/services/ipc.rs) to the agent (crates/openlogi-agent/src/main.rs). The agent then attempts hook installation by calling openlogi_hook::install(), which causes macOS to display the system dialog for the OpenLogi Agent process.

Opening System Settings Directly

For both permissions, OpenLogi provides a utility to open the specific System Settings pane without manual navigation. The open_pane() function in crates/openlogi-permissions/src/macos.rs constructs the appropriate x-apple.systempreferences URL.

use openlogi_permissions::{Permission, open_pane};

// Open Input Monitoring settings
open_pane(Permission::InputMonitoring);

// Open Accessibility settings  
open_pane(Permission::Accessibility);

The implementation maps permission variants to their respective anchors:

  • Permission::Accessibility maps to "Privacy_Accessibility"
  • Permission::InputMonitoring maps to "Privacy_ListenEvent"

Practical Implementation Example

Here is a complete pattern for verifying both permissions before initializing OpenLogi components:

use openlogi_hid::permissions as hid_perm;
use openlogi_permissions::{Permission, open_pane};

/// Verify Input Monitoring and request if missing
async fn ensure_input_monitoring() {
    if !hid_perm::has_access() {
        // Spawn blocking task for the modal dialog
        tokio::task::spawn_blocking(|| {
            hid_perm::request_access();
        }).await.unwrap();
        
        // Offer direct link to settings
        open_pane(Permission::InputMonitoring);
    }
}

/// Request Accessibility via IPC to the agent process
fn trigger_accessibility_prompt() {
    // Desktop sends Command::RequestAccessibilityPrompt to agent
    // Agent executes openlogi_hook::install() to trigger dialog
    open_pane(Permission::Accessibility);
}

Summary

  • Input Monitoring is verified via IOHIDCheckAccess in crates/openlogi-hid/src/permissions.rs and is required for all IOHIDDeviceOpen operations.
  • Accessibility is validated by attempting CGEventTapCreate in crates/openlogi-hook/src/macos.rs, which returns HookError::AccessibilityDenied (line 989) if the grant is missing.
  • Input Monitoring requests block the calling thread and must run inside spawn_blocking contexts to avoid freezing the async runtime.
  • Accessibility prompts originate from the agent process via IPC commands from the desktop GUI, ensuring the dialog appears for the correct binary.
  • Both permission panes can be opened programmatically using open_pane() defined in crates/openlogi-permissions/src/macos.rs.

Frequently Asked Questions

What happens if Input Monitoring is denied?

If Input Monitoring is denied, all IOHIDDeviceOpen calls will fail, preventing OpenLogi from communicating with HID++ devices. The has_access() function returns false, allowing the application to detect the missing permission and direct the user to System Settings before attempting hardware initialization.

Why does Accessibility require an IPC call to the agent?

The Accessibility prompt must originate from the process that creates the event tap. In OpenLogi's architecture, the agent process (openlogi-agent) owns the hook logic in crates/openlogi-hook/src/macos.rs, while the desktop GUI runs as a separate process. The desktop sends Command::RequestAccessibilityPrompt via IPC so the agent can attempt CGEventTapCreate, which triggers the system dialog for the OpenLogi Agent process specifically.

Can these permissions be checked without triggering a dialog?

Yes. IOHIDCheckAccess for Input Monitoring and the success of CGEventTapCreate for Accessibility check the current authorization status silently. Only explicit calls to request_access() or attempts to install the hook trigger the modal system dialogs.

Where are the permission status watchers located?

Real-time Accessibility status is monitored by the watcher in crates/openlogi-agent-core/src/watchers/accessibility.rs. Input Monitoring status is queried on-demand through the PermissionStatus enum exposed in crates/openlogi-permissions/src/macos.rs via the input_monitoring() function.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →