fix(macos): detect screen recording permission after granting access (#130530)

Reuse the existing Peekaboo live permission service so Quick Chat, Settings, screenshot gating, and computer-control diagnostics retain confirmed grants instead of re-reading stale CoreGraphics denials. Passive checks remain consent-gated. Document same-build grant recovery.
This commit is contained in:
Peter Steinberger
2026-08-26 18:20:26 -07:00
committed by GitHub
parent 43be187b67
commit 789a346fa3
3 changed files with 28 additions and 28 deletions
@@ -367,11 +367,13 @@ struct ComputerControlPermissionSnapshot: Equatable, Sendable {
let postEvent: Access
let screenCapture: Access
@MainActor
static func probe() -> Self {
Self(
accessibility: AXIsProcessTrusted() ? .granted : .missing,
postEvent: CGPreflightPostEventAccess() ? .granted : .missing,
screenCapture: CGPreflightScreenCaptureAccess() ? .granted : .missing)
screenCapture: PermissionManager.screenRecordingPermissions.checkScreenRecordingPermission()
? .granted : .missing)
}
var diagnostic: Diagnostic {
@@ -1,11 +1,11 @@
import AppKit
import ApplicationServices
import AVFoundation
import CoreGraphics
import CoreLocation
import Foundation
import Observation
import OpenClawIPC
import PeekabooAutomationKit
import Speech
import UserNotifications
@@ -24,6 +24,8 @@ enum CapabilityAuthorizationStatus: Equatable, Sendable {
}
enum PermissionManager {
@MainActor static let screenRecordingPermissions = PermissionsService()
/// UNUserNotificationCenter.current() aborts with NSInternalInconsistencyException
/// ("bundleProxyForCurrentProcess is nil") in unbundled processes such as
/// `swift build` dev binaries. Every notification-center call must check this
@@ -129,12 +131,12 @@ enum PermissionManager {
return await MainActor.run { AXIsProcessTrusted() }
}
@MainActor
private static func ensureScreenRecording(interactive: Bool) async -> Bool {
let granted = ScreenRecordingProbe.isAuthorized()
if interactive, !granted {
await ScreenRecordingProbe.requestAuthorization()
if interactive, !self.screenRecordingPermissions.checkScreenRecordingPermission() {
self.screenRecordingPermissions.requestScreenRecordingPermission()
}
return ScreenRecordingProbe.isAuthorized()
return await self.screenRecordingPermissions.checkScreenRecordingPermissionLive(forceProbe: interactive)
}
private static func ensureMicrophone(interactive: Bool) async -> Bool {
@@ -251,11 +253,10 @@ enum PermissionManager {
results[cap] = await MainActor.run { AXIsProcessTrusted() } ? .granted : .notGranted
case .screenRecording:
if #available(macOS 10.15, *) {
results[cap] = CGPreflightScreenCaptureAccess() ? .granted : .notGranted
} else {
results[cap] = .granted
}
// CoreGraphics can retain a denial after a grant. Peekaboo retains confirmed grants
// and only unlocks live probes after an explicit permission request.
results[cap] = await self.screenRecordingPermissions.checkScreenRecordingPermissionLive()
? .granted : .notGranted
case .microphone:
results[cap] = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
@@ -485,19 +486,3 @@ final class PermissionMonitor {
self.isChecking = false
}
}
enum ScreenRecordingProbe {
static func isAuthorized() -> Bool {
if #available(macOS 10.15, *) {
return CGPreflightScreenCaptureAccess()
}
return true
}
@MainActor
static func requestAuthorization() async {
if #available(macOS 10.15, *) {
_ = CGRequestScreenCaptureAccess()
}
}
}
+14 -1
View File
@@ -2,6 +2,7 @@
summary: "macOS permission persistence (TCC) and signing requirements"
read_when:
- Debugging missing or stuck macOS permission prompts
- Screen Recording still appears missing after granting access
- Deciding whether to grant Accessibility to node or a CLI runtime
- Packaging or signing the macOS app
- Changing bundle IDs or app install paths
@@ -13,12 +14,24 @@ macOS permission grants are fragile. TCC associates a permission grant with the
## Requirements for stable permissions
- Same path: run a release app from `/Applications/OpenClaw.app`; keep development builds at one fixed path such as `dist/OpenClaw.app`.
- Same bundle identifier: OpenClaw's bundle ID is `ai.openclaw.mac`; changing it creates a new permission identity.
- Same bundle identifier: release builds use `ai.openclaw.mac`; development builds default to `ai.openclaw.mac.debug`. Each has a separate permission identity.
- Signed app: unsigned or ad-hoc signed builds do not persist permissions.
- Consistent signature: use a real Apple Development or Developer ID certificate so the signature stays stable across rebuilds.
Ad-hoc signatures generate a new identity every build. macOS forgets previous grants, and prompts can disappear entirely until the stale entries are cleared.
## Screen Recording still appears missing after granting access
If Quick Chat still shows **Needs additional permissions: Screen Recording**:
1. Click **Grant** in OpenClaw.
2. If macOS opens System Settings, enable the running OpenClaw app under **Privacy & Security -> Screen & System Audio Recording** (called **Screen Recording** on older macOS versions).
3. Return to OpenClaw and retry the screenshot. You can also recheck access with **Settings -> Permissions -> Refresh**.
After an explicit **Grant** request, OpenClaw checks ScreenCaptureKit as well as the macOS permission preflight. This lets it recognize access when the preflight still reports an old denial. Passive status checks do not initiate this probe before you request access.
If access still appears missing, quit and reopen OpenClaw from the same app path. Some macOS permission changes require an app restart before capture works. If both release and development builds are installed, grant access to the build you are actually running: approving `/Applications/OpenClaw.app` does not grant access to a development build with a different bundle identifier.
## Accessibility grants for Node and CLI runtimes
Prefer granting Accessibility to OpenClaw.app, Peekaboo.app, or another signed helper with its own bundle identifier instead of a generic `node` binary.