How OpenLogi Handles macOS TCC Permission Checks Without User Prompts
OpenLogi avoids macOS Transparency, Consent, and Control (TCC) prompts by isolating Accessibility and Input Monitoring privileges into a separate, code-signed helper agent that runs under its own launchd identity, making it—not the GUI—the TCC-responsible process.
OpenLogi is an open-source automation framework that requires elevated system privileges to function on macOS. Managing macOS TCC permission checks traditionally forces applications to display intrusive user prompts, but OpenLogi implements a sophisticated architectural pattern that grants and retains these permissions silently. By decoupling the GUI from privileged operations through a dedicated background agent, the system maintains a seamless user experience while satisfying Apple's strict security requirements.
Architectural Overview: The Helper Agent Pattern
OpenLogi isolates TCC-sensitive components—specifically Accessibility and Input Monitoring permissions—into a distinct binary called the agent. Unlike monolithic applications that bundle these privileges into the main GUI process, OpenLogi keeps the GUI as a thin client. The agent runs as a separate process with its own launchd label, ensuring that macOS attributes TCC grants to the agent's code signature rather than the parent application.
This separation creates three critical advantages:
- No GUI Prompt Chain: The GUI never becomes the "responsible process" for the TCC grant, so it never triggers a user-facing system dialog.
- Persisted Grants: The grant is stored against the signed helper bundle's identifier. Subsequent updates of the GUI do not affect the grant because the helper's bundle identifier remains constant.
- Automatic Renewal: If a grant is revoked, OpenLogi detects the revocation via a watchdog mechanism and can relaunch the helper under the same launchd label, preserving the TCC identity.
Launching the Agent as the TCC-Responsible Process
In crates/openlogi-desktop/src/services/ipc/launch.rs, OpenLogi launches the helper binary using low-level POSIX spawn attributes to ensure it does not inherit the GUI's execution context. The implementation comments explicitly note this creates a "TCC responsible process" where a direct exec attributes its Accessibility permissions to the helper alone.
The code uses posix_spawnattr_setflags with POSIX_SPAWN_SETEXEC to replace the current process image, preventing the GUI's launch context from contaminating the agent's TCC identity:
use std::process::Command;
use std::os::unix::process::CommandExt;
// Path to the signed helper binary
let helper_path = "/Applications/OpenLogi.app/Contents/MacOS/openlogi-agent";
// Direct exec – prevents inheritance of the GUI's launch context
let mut cmd = Command::new(helper_path);
unsafe {
// Set POSIX_SPAWN_SETEXEC to replace the current process image
cmd.pre_exec(|| {
libc::posix_spawnattr_setflags(&mut libc::posix_spawnattr_t::default(),
libc::POSIX_SPAWN_SETEXEC);
Ok(())
});
}
let _ = cmd.spawn();
By launching the agent directly under its own Label defined in a launchd plist installed at ~/Library/LaunchAgents, macOS treats the agent as an independent service. This plist includes the EnableTransactions flag and the helper bundle's CFBundleIdentifier, which macOS uses as the persistent key for TCC records.
Code Signing Strategies for Grant Persistence
According to xtask/src/commands/macos/bundle/signing.rs, OpenLogi explicitly signs the helper binary to ensure TCC grants persist across application updates. The signing process preserves the TeamIdentifier between the GUI and the agent, allowing macOS to treat them as a single logical application while maintaining separate TCC contexts.
The signing command implemented in the xtask build system ensures the helper maintains its Accessibility privileges:
cargo xtask macos bundle sign --identity "Developer ID Application: Your Name (TEAMID)" \
--output openlogi-agent-signed
As detailed in xtask/src/commands/macos/bundle/identity.rs, macOS keys TCC grants (Accessibility, Input Monitoring) to a bundle's code signature and identifier. Because the helper always launches via its dedicated launchd plist with a consistent CFBundleIdentifier, the grant survives even when the main OpenLogi application updates or changes its own binary signature.
Monitoring and Recovering from Permission Revocation
The crates/openlogi-hook/src/macos.rs file contains a watchdog thread that monitors the CoreGraphics event tap for stalls. When TCC permissions are revoked, macOS can wedge the tap thread inside CoreGraphics operations. OpenLogi's hook detects this stall state ("The tap thread is wedged") and gracefully aborts the tap.
Upon detecting revocation, the system can automatically relaunch the helper under the same launchd label. Because the TCC grant is tied to the agent's persistent bundle identity rather than a runtime process ID, the new instance immediately regains the necessary privileges without requiring user intervention or displaying new permission dialogs.
Silent Permission Queries with openlogi-permissions
To check grant status without triggering system prompts, OpenLogi uses the openlogi-permissions crate. The file crates/openlogi-permissions/src/macos.rs provides APIs to query the current TCC status for Accessibility and Input Monitoring entirely from the helper process, keeping the GUI unaware of permission states until access is confirmed.
use openlogi_permissions::macos::{AccessibilityStatus, InputMonitoringStatus};
fn print_tcc_status() {
println!("Accessibility: {:?}", AccessibilityStatus::current());
println!("Input Monitoring: {:?}", InputMonitoringStatus::current());
}
This allows the agent to report its authorization state to the GUI via IPC, ensuring the user interface never attempts to trigger a TCC check that would generate a prompt.
Summary
- Process Isolation: OpenLogi places TCC-sensitive operations in a separate agent process launched via its own launchd service, preventing the GUI from becoming the TCC-responsible process.
- Direct Execution: The agent launches using
posix_spawnattr_setflagswith execution attributes that prevent inheritance of the GUI's security context, as implemented incrates/openlogi-desktop/src/services/ipc/launch.rs. - Persistent Identity: Code signing in
xtask/src/commands/macos/bundle/signing.rsensures the agent'sTeamIdentifierandCFBundleIdentifierremain constant, allowing TCC grants to persist across application updates. - Revocation Handling: A watchdog in
crates/openlogi-hook/src/macos.rsdetects TCC revocation via CoreGraphics stalls and triggers agent relaunch to restore privileges automatically. - Silent Queries: The
openlogi-permissionscrate enables background permission status checks without prompting the user, keeping the GUI experience seamless.
Frequently Asked Questions
Why does separating the agent prevent TCC prompts?
macOS attributes TCC grants to the process that directly requests the privileged operation. By executing the agent via direct execve-style attributes using posix_spawnattr_setflags, OpenLogi ensures the agent—not the launcher—becomes the TCC-responsible process. Because the agent runs headless without a GUI, macOS grants permissions based on its code signature alone, bypassing the standard user prompt chain that would occur if the GUI made the request.
How does OpenLogi survive application updates without losing permissions?
The TCC system stores grants against a combination of the code signature and bundle identifier, not the specific binary file. In xtask/src/commands/macos/bundle/identity.rs, OpenLogi ensures the helper agent maintains a constant CFBundleIdentifier across builds. As long as the updated agent retains the same Team ID and bundle ID, macOS recognizes it as the previously authorized entity, preserving the Accessibility and Input Monitoring grants without requiring re-authorization.
What happens if the user revokes TCC permissions while OpenLogi is running?
When permissions are revoked via System Settings, macOS immediately stalls any active CoreGraphics event taps in crates/openlogi-hook/src/macos.rs. OpenLogi's watchdog thread detects this wedge state, terminates the blocked tap, and signals the desktop service to relaunch the agent. Because the relaunch uses the same launchd label and signed bundle, the agent immediately restarts with a clear permission state, ready to function if the user re-grants access or to idle safely if access remains denied—all without GUI prompts.
Can this pattern be used by any macOS application?
While the architectural pattern of isolating privileges in a launchd-managed helper is generally applicable, OpenLogi's implementation relies on specific code-signing configurations and POSIX spawn attributes detailed in crates/openlogi-desktop/src/services/ipc/launch.rs. Applications adopting this approach must ensure their helper binaries are properly signed with consistent team identifiers and launched via dedicated launchd plists in ~/Library/LaunchAgents. However, Apple's App Store guidelines may restrict certain privilege isolation techniques, so developers should verify compliance requirements before implementing similar TCC bypass strategies in commercial software.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →