I have a bit of an unconventional “Work From Cafe” (WFC) setup.
While most people carry a MacBook, I often travel with a Mac Mini and a tablet.
You might ask: Why carry a desktop to a cafe?
The answer comes down to pure economics. A Mac Mini with an M4-series chip offers the exact same raw performance as a MacBook Pro but for a fraction of the price. We are talking about a ~$600 machine versus a $2,000+ laptop.
Furthermore, I already own a tablet. Why should I pay for a MacBook screen I don’t need when I already have a gorgeous, high-resolution display in my backpack?
To bridge the two, I don’t use Sidecar or VNC. I use Sunshine.
Sunshine (the host for Moonlight clients) is designed for gaming. That means it prioritizes ultra-low latency above all else. For coding and general UI navigation, it feels almost native, whereas standard VNC or AirPlay can feel like moving your mouse through molasses.
But after the recent update to macOS Tahoe (macOS 26), my perfect, budget-friendly setup broke.
The Problem: The “Invisible” Service
Everything was perfect until the recent macOS Tahoe update.
In previous versions of macOS, if you ran Sunshine as a background service or command-line tool, it might have been flaky, but at least it was visible. You could go into System Settings, find the binary, and toggle “Screen Recording” on.
macOS Tahoe changes the rules entirely.
The new security policy doesn’t just forget permissions for command-line tools, it ignores them completely. If you run a raw binary or a background service:
- No Prompt: The system will not prompt you to allow screen recording.
- No UI Entry: If you manually check
System Settings > Privacy & Security > Screen Recording, the service will not appear. You cannot toggle a permission that doesn't exist. - No Manual Add: You often cannot even manually drag a binary into the list.
The system now essentially demands that any process requesting screen pixels must be a proper Native App Bundle with a valid Info.plist and Bundle Identifier. Without that identity, the service is invisible to the permission system.
The Solution: Building a Native Wrapper
To fix this, I wrote a script that generates a “real” macOS application on the fly. You don’t need Xcode installed just the terminal.
This wrapper wraps the Sunshine binary inside a legitimate .app structure. This gives it a stable Bundle Identity (dev.lizardbyte.sunshine.wrapper), which forces macOS to recognize it as a valid application, triggering the permission prompt and allowing it to appear in the System Settings list.
The Generator Script
Here is the script I used to restore my WFC setup. It compiles a tiny Swift application that acts as a bridge between the OS and the Sunshine binary.
Step 1: Create the file Open your Terminal and create a file named sunshine_wrapper.sh:
touch sunshine_wrapper.sh
Step 2: Paste the Code Paste the following code.
Note: The Swift code below includes specific logic to handle the “Quit” command. This ensures that when you quit the wrapper app, it kills the background process, preventing “zombie” instances from eating up your CPU.
#!/bin/bash
# Configuration
APP_NAME="Sunshine"
APP_DIR="${APP_NAME}.app"
BUNDLE_ID="dev.lizardbyte.sunshine.wrapper"
# Attempt to find sunshine binary
SUNSHINE_BIN=$(which sunshine)
if [ -z "$SUNSHINE_BIN" ]; then
if [ -f "/opt/homebrew/bin/sunshine" ]; then
SUNSHINE_BIN="/opt/homebrew/bin/sunshine"
else
echo "❌ Error: 'sunshine' binary not found!"
exit 1
fi
fi
echo "✅ Targeting Sunshine binary at: $SUNSHINE_BIN"
# 1. Clean & Create Structure
echo "📂 Creating App Structure..."
rm -rf "$APP_DIR"
mkdir -p "${APP_DIR}/Contents/MacOS"
mkdir -p "${APP_DIR}/Contents/Resources"
# 2. Create the Robust Swift Launcher
# This version imports Cocoa to handle the Quit command properly.
SWIFT_SOURCE="Launcher.swift"
cat <<EOF > "$SWIFT_SOURCE"
import Cocoa
import Foundation
class AppDelegate: NSObject, NSApplicationDelegate {
var process: Process!
func applicationDidFinishLaunching(_ notification: Notification) {
let sunshinePath = "$SUNSHINE_BIN"
process = Process()
process.executableURL = URL(fileURLWithPath: sunshinePath)
process.arguments = CommandLine.arguments.dropFirst().map { String(\$0) }
// Pipe output so you can debug via Console.app if needed
process.standardOutput = FileHandle.standardOutput
process.standardError = FileHandle.standardError
// If Sunshine crashes/quits on its own, close the wrapper app too
process.terminationHandler = { _ in
NSApp.terminate(nil)
}
do {
try process.run()
} catch {
print("Failed to launch sunshine: \(error)")
NSApp.terminate(nil)
}
}
// This is called when you Right Click -> Quit in the Dock
func applicationWillTerminate(_ notification: Notification) {
if let proc = process, proc.isRunning {
// Send SIGTERM to Sunshine to shut it down gracefully
proc.terminate()
// Wait a moment for it to cleanup, or it becomes a zombie
proc.waitUntilExit()
}
}
}
// Main entry point
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.regular) // Shows in Dock, has UI (menu bar)
app.run()
EOF
# 3. Compile
echo "🔨 Compiling Native Wrapper (with AppKit)..."
swiftc "$SWIFT_SOURCE" -o "${APP_DIR}/Contents/MacOS/${APP_NAME}"
rm "$SWIFT_SOURCE"
# 4. Create Info.plist (Updated for Icon)
cat <<EOF > "${APP_DIR}/Contents/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>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.0</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
</dict>
</plist>
EOF
# 5. Sign
echo "🔐 Signing the application..."
codesign --force --deep --sign - "${APP_DIR}"
echo "------------------------------------------------"
echo "✅ Success! '${APP_DIR}' created."
echo "------------------------------------------------"
Step 3: Run and Install
Run the script:
chmod +x sunshine_wrapper.sh
./sunshine_wrapper.sh
Then, move the resulting Sunshine.app to your /Applications folder.
The Result
After running this script, you may need to reset your permissions one last time to clear out any old conflicts:
tccutil reset ScreenCapture dev.lizardbyte.sunshine.wrapper
When you launch the new app, macOS Tahoe will see a signed, native application requesting access. Click Allow, and the permission will finally stick.
My portable Mac Mini setup is back in business. Just like that, you can stream your Mac Mini to Moonlight or Artemis on your tablet with zero fuss and low latency. If you are struggling with screen sharing tools on the new macOS, try wrapping them in a native .appit makes all the difference.
Top comments (0)