How OpenLogi Handles macOS TCC Access: Architecture and Implementation

OpenLogi isolates privileged macOS capabilities in a separate agent process that becomes the TCC-responsible entity, allowing persistent grants independent of the GUI lifecycle while providing non-intrusive permission status queries.

OpenLogi, an open-source automation framework maintained by AprilNEA, implements a strict macOS TCC (Transparency, Consent, and Control) access model to comply with Apple’s privacy sandbox requirements. The architecture deliberately separates the graphical interface from privileged system operations to ensure that accessibility and input monitoring grants survive application restarts. This article examines the source code to explain how OpenLogi manages macOS TCC access through process isolation, launch-time orchestration, and programmatic permission handling.

TCC-Responsible Process Architecture

macOS ties TCC grants to the code signature of the process that first requests the permission, meaning the entity that triggers the consent dialog owns the grant permanently. OpenLogi ensures that the agent binary (openlogi-agent)—not the GUI (openlogi-desktop)—becomes the TCC-responsible process for Accessibility and Input Monitoring.

Agent Launch via LaunchServices

In crates/openlogi-desktop/src/services/ipc/launch.rs, the spawn_agent function (lines 36-44) explicitly launches the agent via LaunchServices using the command open -g -n <path>. This creates a distinct process with its own bundle identity and code signature, ensuring the agent "is its own TCC responsible process." For development builds, the system falls back to a direct exec call, which deliberately disclaims the GUI’s TCC identity to prevent the agent from inheriting the parent’s permission context.

Why the GUI Cannot Own the Grant

If the GUI owned the Accessibility grant, macOS would revoke the permission when the GUI process exits, immediately breaking the persistent input hook required for automation. By isolating the hook in the agent and launching it as a separate bundle, the grant remains valid for the lifetime of the agent process, independent of the GUI’s process lifecycle. This separation is critical for maintaining continuous input monitoring across user sessions.

Querying Permission Status Without Prompting

OpenLogi reads current TCC statuses through non-prompting APIs defined in crates/openlogi-permissions/src/macos.rs. The module exposes specific functions for each privileged capability, returning a PermissionStatus enum (Granted, Denied, or Unknown) without triggering system dialogs.

  • input_monitoring(): Uses IOHIDCheckAccess(IOHIDRequestType::ListenEvent) to query Input Monitoring status.
  • bluetooth(): Invokes CBManager.authorization via the Objective-C runtime to check Bluetooth authorization.
  • camera(): Delegates to openlogi-camera::camera_authorization() for Camera access status.
  • open_pane(Permission): Opens the appropriate System Settings privacy pane using the x-apple.systempreferences URL scheme (lines 68-76) when the user needs to modify a grant manually.

The URL mapping specifically anchors to Privacy_Accessibility, Privacy_ListenEvent, Privacy_Bluetooth, or Privacy_Camera depending on the permission type.

Launch-Time TCC Handling and Launchd Integration

When the GUI detects that the agent socket is missing, it attempts to kick-start the registered launchd service through kickstart_registered_agent (lines 15-52 in launch.rs). If that fails, the system calls ensure_registered to register the agent as a login item on-demand, then retries the kick-start.

Both paths use the system-wide launchd domain gui/<uid>/…, which automatically confers a unique TCC identity upon the launched agent. Production builds rely on this supervised service model to guarantee correct TCC attribution, while development profiles fall back to direct open calls that bypass launchd registration but still maintain process isolation.

Code Examples

The following patterns demonstrate how OpenLogi interacts with the TCC subsystem programmatically:

// Query Input Monitoring status without prompting
use openlogi_permissions::macos::{input_monitoring, PermissionStatus};

match input_monitoring() {
    PermissionStatus::Granted => println!("Input Monitoring active"),
    PermissionStatus::Denied => eprintln!("Permission denied"),
    PermissionStatus::Unknown => println!("Status undetermined"),
}
// Direct user to the Accessibility permission pane
use openlogi_permissions::{Permission, open_pane};
open_pane(Permission::Accessibility);
// Launch agent with independent TCC identity (macOS)
fn launch_agent(path: &std::path::Path) -> std::io::Result<()> {
    std::process::Command::new("/usr/bin/open")
        .args(&["-g", "-n"])
        .arg(path)  // Path to .app bundle
        .spawn()?;
    Ok(())
}

Summary

  • Process Isolation: The openlogi-agent binary is launched via LaunchServices or direct exec to establish its own code signature as the TCC-responsible entity, separate from the GUI.
  • Non-Intrusive Queries: Permission status is checked via native macOS APIs (IOHIDCheckAccess, CBManager.authorization) without triggering consent dialogs.
  • System Settings Integration: When user intervention is required, OpenLogi opens the specific privacy pane using x-apple.systempreferences URL schemes.
  • Launchd Orchestration: Production builds use launchd’s supervised service domain (gui/<uid>/…) to ensure persistent TCC identity across restarts, while dev builds use fallback execution paths.

Frequently Asked Questions

Why does OpenLogi use a separate agent process for TCC instead of the main GUI?

The agent process maintains persistent TCC grants independent of the GUI lifecycle. If the GUI owned the Accessibility grant, macOS would revoke the permission when the GUI exits, breaking automation hooks. The separate process ensures that input monitoring remains active even when the user interface restarts.

How does OpenLogi check if Input Monitoring permission is granted without triggering a system prompt?

The system calls IOHIDCheckAccess(IOHIDRequestType::ListenEvent) through the input_monitoring() function in crates/openlogi-permissions/src/macos.rs. This API returns the current authorization status silently without presenting a consent dialog to the user.

What happens if the agent fails to launch via launchd in a production build?

If kickstart_registered_agent fails, OpenLogi invokes ensure_registered to register the agent as a login item and then attempts to kick-start again. This on-demand registration ensures that the agent eventually launches with the correct launchd context and TCC identity, even if the initial service lookup fails.

How does OpenLogi ensure TCC grants persist after application updates?

The project uses code signing utilities defined in xtask/src/commands/macos/bundle/signing.rs and xtask/src/commands/macos/bundle/identity.rs to maintain consistent bundle identifiers and entitlements across releases. Since macOS TCC grants are tied to the code signature, consistent signing ensures that updates do not invalidate existing user consents.

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 →