-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Fix: CGDisplayCreateImage returns desktop wallpaper instead of screen content
Problem
When running agent-watch daemon as a background process (via launchd or an .app bundle), CGDisplayCreateImage(CGMainDisplayID()) in FrameBufferStore.captureFrame() silently returns the desktop wallpaper instead of the actual screen content.
This affects all macOS users running apps in fullscreen Spaces — the frame buffer only contains the desktop compositing layer, not the active window.
Root Cause
DaemonRunner.run() uses RunLoop.main.run() to keep the process alive. This does not establish a connection with the WindowServer, so macOS TCC (Transparency, Consent, and Control) does not recognize the process as "responsible code" — screen recording permission is effectively ignored, and CGDisplayCreateImage returns only the desktop wallpaper layer.
Reference: https://developer.apple.com/forums/thread/694948
Fix
Two changes in Sources/ScreenTextKit/Capture/DaemonRunner.swift:
- Initialize
NSApplication.sharedwith.accessoryactivation policy before setting up capture timers - Replace
RunLoop.main.run()withapp.run()
public func run() -> Never {
// Register with WindowServer for proper screen capture
let app = NSApplication.shared
app.setActivationPolicy(.accessory)
// ... existing observer + timer setup unchanged ...
capture(trigger: .manual)
logger.info("Daemon started")
app.run() // was: RunLoop.main.run()
fatalError("Run loop exited unexpectedly")
}Why this works
NSApplication.sharedregisters the process with the WindowServer.setActivationPolicy(.accessory)makes it a proper background app (no dock icon) that macOS recognizes for TCC purposesapp.run()starts the NSApplication run loop, which maintains the WindowServer connection — existingTimer.scheduledTimercalls continue to work as before
Optional: .app bundle for TCC identification
For best results, the binary should run inside a minimal .app bundle so that macOS TCC can properly identify it as the "responsible code". A minimal Info.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>agent-watch</string>
<key>CFBundleIdentifier</key>
<string>com.differentai.agentwatch.app</string>
<key>CFBundleName</key>
<string>AgentWatch</string>
<key>LSUIElement</key>
<true/>
<key>NSScreenCaptureUsageDescription</key>
<string>AgentWatch needs screen recording access to capture on-screen text.</string>
</dict>
</plist>The binary should be the direct CFBundleExecutable (not wrapped in a shell script), otherwise TCC may not trace the responsible code back to the app bundle.
Testing
- Before fix: all frame buffer captures are identical file sizes (same wallpaper image regardless of active app)
- After fix: frame buffer captures vary in size and contain actual screen content, including fullscreen apps
Environment
- macOS Tahoe
- Multiple fullscreen Spaces
- Screen Recording permission granted