Skip to content

fix: CGDisplayCreateImage returns desktop wallpaper instead of screen content #1

@Korkyzer

Description

@Korkyzer

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:

  1. Initialize NSApplication.shared with .accessory activation policy before setting up capture timers
  2. Replace RunLoop.main.run() with app.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.shared registers the process with the WindowServer
  • .setActivationPolicy(.accessory) makes it a proper background app (no dock icon) that macOS recognizes for TCC purposes
  • app.run() starts the NSApplication run loop, which maintains the WindowServer connection — existing Timer.scheduledTimer calls 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions