Skip to content

Feature: Trusted Automation allowlist (per-process bypass for AppleScript and MCP-driven workflows) #45

Description

@robertvitali

Use case

I've been running MakLock on my home Mac Studio for walk-up defense. The Studio sits with auto-login enabled because FileVault + auto-login are mutually exclusive on macOS, and LaunchAgents need to recover unattended after a weekly maintenance restart. Tier 1 lock list: Messages, Mail, Photos, Notes, iPhone Mirroring, Contacts, FaceTime, Passwords. MakLock has been the best tool I evaluated: menubar UX, panic shortcut, Watch unlock, and the auditability of an open-source codebase all hit the right balance.

The collision: I also drive these same apps via Model Context Protocol servers from AI agent clients (Claude Code, Claude Desktop, OpenAI Codex CLI). The MCPs use AppleScript under the hood. When an agent calls tell application "Mail" to ..., Mail launches/activates, MakLock's overlay correctly fires, and the AppleScript times out at 60s before any human is around to authenticate. The Apple-suite MCPs affected on my fleet:

Workarounds today are all bad: (1) remove Mail/Messages/etc. from the locked list (defeats the point), (2) manually unlock each app before every agent session (high friction), or (3) skip Apple-suite MCPs entirely (loses native multi-account Mail.app integration).

The design vision

What I want mirrors macOS's existing Privacy & Security allowlists (Full Disk Access, Automation, Accessibility): a GUI-managed allowlist inside MakLock of trusted automation clients. When a locked app is activated and an allowlisted agent is the cause, MakLock recognizes the trusted client and bypasses the lock for that operation. Walk-up defense stays intact; agent automation works without friction. Mental model is already familiar: "I added Claude Code to MakLock's Trusted Automation list, the same way I added it to Full Disk Access."

Bonus UX: when MakLock launches the locked app on behalf of an allowlisted agent, the app launches window-hidden (no UI disruption) and auto-quits when the agent's automation traffic ends. The user never sees Mail.app pop in and out.

What I looked at and ruled out

I read through MakLock/Core/Services/AppMonitorService.swift, Core/Managers/SafetyManager.swift, and the macOS TCC architecture before writing this. Several tempting options that don't actually work:

  • Register MakLock as a TCC service. macOS's kTCCService* IDs are private to Apple; tccd ignores any unknown service strings, and TCC.db is SIP-protected. Cannot insert a "MakLock Bypass" pane into System Settings; the pattern has to be replicated inside MakLock's own UI.
  • Approve-on-AppleEvent-prompt at the OS layer would be ideal: a per-event Touch ID prompt instead of a per-app one. But macOS doesn't expose AEDeterminePermissionToAutomateTarget callback hooks publicly, and Endpoint Security (which would tell you the exact PID that originated an Apple Event) needs the restricted system extension entitlement Apple grants narrowly.
  • NSWorkspace activation-source detection. NSWorkspace's userInfo only carries applicationUserInfoKey; there's no "this activation was AppleScript-triggered" hint.

What's left is heuristic process attribution: identify that an allowlisted agent is currently running, attribute the activation to it, and bypass on that basis. The honest framing is the 1Password CLI biometric unlock pattern: trust is rooted in a human authentication act inside the trusted agent's process tree, then propagated to children. That's what we replicate.

Proposed design: Trusted Automation tab

Allowlist UX

New Settings tab Trusted Automation. Each entry stores:

  • Code-signing team ID + bundle identifier (canonical primary key, matched via SecCodeCopySigningInformation to defeat trivial spoofing). This mirrors how LuLu and Little Snitch identify peers; signing identity beats path or PID.
  • Friendly name + app icon (cosmetic).
  • Bypass toggle (default off when adding a new entry).
  • Auto-hide windows checkbox (default on).
  • Auto-quit when agent exits checkbox (default on).

User adds an entry by clicking "Add…" → file picker → MakLock runs SecCodeCopySigningInformation on the chosen .app and prefills the team ID/bundle ID/name/icon. Pre-bundled defaults shipped disabled-by-default for com.anthropic.ClaudeCode, com.openai.codex, Cursor, iTerm, Terminal.app. User explicitly opts in per agent.

Detection mechanism (the honest version)

A running allowlisted agent is not the same as an active one. To attribute an activation honestly, MakLock checks two conditions before bypassing:

  1. Allowlisted agent is currently running (signing team ID matches an enabled allowlist entry). Verified via NSWorkspace.shared.runningApplications snapshot + SecCodeCopySigningInformation on the running app's executableURL.
  2. Agent has set MAKLOCK_TRUSTED=1 in its process tree environment. Read via proc_pidinfo(pid, PROC_PIDENVINFO, ...). The agent (or its launcher script) sets this env var only when it intends to do automation work; running an allowlisted app casually (the user just opened Claude Code's GUI to chat) doesn't grant bypass.

This is the 1Password unlock model exactly: human authentication act inside the trusted process tree, propagated to children. The env var is what makes it honest. Without it, "Claude Code is running" alone is too permissive: the user might have opened the GUI and now expects Mail to lock if anyone walks up.

Two-condition check inserted into AppMonitorService.handleAppEvent (Core/Services/AppMonitorService.swift:170-199) before pendingLockBundleIDs.insert. New service Core/Services/TrustedAutomationService.swift owns the logic.

Auto-hide and auto-quit orchestration

When the bypass fires:

  1. If app is already running → leave it; mark authenticated; arm quiescence timer.
  2. If app is launching → in the didLaunchApplicationNotification callback, immediately call NSRunningApplication.hide(). There's a brief Dock-bounce flash (~50-150ms) but no persistent visible window.
  3. Track agent's PID via kqueue EVFILT_PROC NOTE_EXIT. When the agent process exits, that's a hard quiescence signal: terminate Mail (if MakLock launched it; track that flag) or just clear authentication (if user already had Mail open).
  4. Quiescence timer fallback for cases where the agent stays alive (e.g., long-running Claude Code session): 30s sliding window reset on each didActivateApplicationNotification for the locked app. Timer expiry triggers same termination/clear-auth logic.

Touchpoints

  • New Models/TrustedAgent.swift (Codable: teamID, bundleID, friendlyName, iconData, isEnabled, autoHide, autoQuit).
  • New Core/Services/TrustedAutomationService.swift (allowlist storage, running-process check, env-var read via proc_pidinfo, kqueue-based exit observation).
  • New UI/Settings/TrustedAutomationSettingsView.swift (Settings tab with allowlist editor).
  • Core/Storage/Defaults.swift: add trustedAgents: [TrustedAgent] next to protectedApps.
  • Core/Services/AppMonitorService.swift (lines 170-199): two-condition check before queueing the overlay; if pass, call new markAuthenticatedViaAutomation(bundleID:agentPID:) which seeds the kqueue observer.
  • Core/Managers/SafetyManager.swift: factor existing systemBlacklist pattern (lines 34-53) so the new allowlist sits alongside it visually + architecturally.
  • App/AppDelegate.swift: wire TrustedAutomationService lifecycle.

Estimated ~250 LOC. Half-day to one day of work. No new entitlements, no private APIs. Survives Sparkle upgrades because allowlist persists in UserDefaults.

Edge cases / security

Scenario Behavior
User opens Mail manually while Claude Code is running but MAKLOCK_TRUSTED is unset Lock fires normally (env-var gate prevents false bypass)
Two allowlisted agents running, both with MAKLOCK_TRUSTED=1 Both grant bypass; either's exit triggers quiescence cleanup
Agent crashes mid-AppleScript NOTE_EXIT fires, Mail terminated/auth cleared; next manual launch locks again
Agent forgets to set MAKLOCK_TRUSTED Bypass doesn't fire; same as no allowlist (lock works normally)
Attacker drops a binary signed with stolen team ID Signing team ID is the primary key; team ID compromise is its own (much larger) problem
Attacker creates an app with the same bundle ID but different team ID Doesn't match; no bypass
MakLock relaunches mid-bypass Allowlist persists in UserDefaults; running-process scan re-runs on launch
System sleeps during bypass Existing SleepWakeService.onSleep clears authentication; bypass re-evaluates on wake
Sparkle auto-update mid-session Setting survives (per-bundle-ID UserDefaults)

Threat model: this expands "what an attacker who has compromised a trusted agent can do," not "what an attacker who can authenticate as the user can do." The user explicitly added the agent's signing team ID to the allowlist; that's the trust act. Walk-up defense is unaffected: a passer-by who doesn't have a running allowlisted-agent process with MAKLOCK_TRUSTED=1 set in its env still hits the lock screen.

Manual QA checklist (matching the project's testing energy; no test target in repo)

  • Add Claude Code to allowlist; toggle bypass on; enable auto-hide and auto-quit. Lock Mail.
  • From terminal: MAKLOCK_TRUSTED=1 osascript -e 'tell application "Mail" to count of inboxes'. Mail launches hidden, AppleScript returns count, Mail quits within ~30s.
  • Same osascript without MAKLOCK_TRUSTED=1: lock overlay fires.
  • Open Mail manually (Spotlight) while Claude Code is running: lock overlay fires (no env var).
  • Two-agent test: run both Claude Code and Codex with MAKLOCK_TRUSTED=1; bypass works; killing one keeps bypass active until the other exits.
  • Sleep + wake during active bypass. Auth cleared; next agent activation re-evaluates.
  • Panic key during active bypass. All bypasses revoked.
  • Force-quit MakLock during active bypass; relaunch. Allowlist persists; running-process scan re-runs.
  • Sparkle update during active bypass. Setting survives.

Future follow-ups (separate issues if/when this lands)

  • XPC handshake variant. Cryptographically honest version: agent connects to a MakLock Mach service, MakLock reads audit_token_t from the connection peer, validates via SecCodeCreateWithAuditToken. Stronger than env-var attribution but adds an SDK contract MCP authors have to adopt. ~700 LOC.
  • Per-app bypass scope. Today the allowlist is global per-agent. v1.1 could let users specify "Claude Code can bypass Mail and Calendar but not Passwords."

Precedent in your codebase and ecosystem

PR #39 (external-SSD-conditional locking) is the same shape (condition-based bypass), and SafetyManager.systemBlacklist (SafetyManager.swift:34-53) is the existing pattern for "skip the overlay when X is true." This proposal adds a parallel allowlist alongside it. Roadmap items (Trusted Wi-Fi unlock v1.1, per-window overlay v1.2) reinforce the conditional-bypass direction.

The signing-identity-as-primary-key pattern is what LuLu uses for its outbound-connection allowlist; the env-var trust signal is what 1Password CLI uses for biometric unlock inheritance into sub-shells. Both are battle-tested in the macOS security tools space.

Offer

If this design fits the way you want MakLock to grow, I'd be happy to send the PR, including the env-var attribution path, auto-hide/auto-quit orchestration, and the QA matrix in the description. If you'd prefer a different shape (different UX, different scope, different name, different attribution mechanism), tell me and I'll redesign before writing code. If this isn't a feature you want at all, that's a fair answer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions