How I Fixed Sunshine Screen Sharing on macOS Tahoe (The Native Wrapper Method)
Source: Dev.to
Why a Mac Mini?
- Performance: An M4‑series Mac Mini delivers the same raw performance as a MacBook Pro.
- Cost: ~ $600 vs. $2 000+ for a comparable laptop.
- Display: I already own a high‑resolution tablet, so I don’t need a laptop screen.
How I Connect the Mini to the Tablet
I don’t use Sidecar or VNC. I use Sunshine (the host for Moonlight clients).
Sunshine is built for gaming, so it prioritises ultra‑low latency. For coding and UI navigation it feels almost native—much smoother than standard VNC or AirPlay.
The Problem: macOS 26 (Tahoe) Breaks the Setup
After updating to macOS 26 “Tahoe”, the following changed:
| What used to work | What happens now |
|---|---|
| Run Sunshine as a background service / CLI tool → System Settings showed the binary and let you enable Screen Recording. | No prompt appears. The binary never shows up in System Settings → Privacy & Security → Screen Recording. You can’t add it manually. |
| Permissions were flaky but at least visible. | The system now requires a proper native app bundle (with an Info.plist and bundle identifier). Raw binaries are invisible to the permission system. |
Bottom line
Any process that wants to capture screen pixels must be a real macOS app bundle.
The Fix: A Tiny Wrapper App
I wrote a script that creates a legitimate .app bundle on‑the‑fly.
The wrapper:
- Packages the Sunshine binary inside a proper app bundle.
- Gives the bundle a stable identifier (
dev.lizardbyte.sunshine.wrapper). - Triggers the macOS screen‑recording permission prompt.
- Handles the Quit command so the Sunshine process is terminated cleanly (no zombie processes).
You only need Terminal and the Swift toolchain—no Xcode required.
Step‑by‑Step Script
1️⃣ Create the script file
touch sunshine_wrapper.sh
2️⃣ Paste the following code into sunshine_wrapper.sh
#!/bin/bash
# -------------------------------------------------
# Configuration
# -------------------------------------------------
APP_NAME="Sunshine"
APP_DIR="${APP_NAME}.app"
BUNDLE_ID="dev.lizardbyte.sunshine.wrapper"
# -------------------------------------------------
# Locate the 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 the .app structure
# -------------------------------------------------
echo "📂 Creating App Structure..."
rm -rf "$APP_DIR"
mkdir -p "${APP_DIR}/Contents/MacOS"
mkdir -p "${APP_DIR}/Contents/Resources"
# -------------------------------------------------
# 2. Write the Swift launcher source
# -------------------------------------------------
SWIFT_SOURCE="Launcher.swift"
cat "$SWIFT_SOURCE"
import Cocoa
import Foundation
class AppDelegate: NSObject, NSApplicationDelegate {
var process: Process!
func applicationDidFinishLaunching(_ notification: Notification) {
// Path to the real Sunshine binary (filled in by the Bash script)
let sunshinePath = "$SUNSHINE_BIN"
process = Process()
process.executableURL = URL(fileURLWithPath: sunshinePath)
// Forward any arguments passed to the wrapper onto Sunshine
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 exits on its own, quit the wrapper too
process.terminationHandler = { _ in
NSApp.terminate(nil)
}
do {
try process.run()
} catch {
print("Failed to launch sunshine: \(error)")
NSApp.terminate(nil)
}
}
// Called when the user quits the wrapper (e.g., Right‑Click → Quit)
func applicationWillTerminate(_ notification: Notification) {
if let proc = process, proc.isRunning {
// Gracefully stop Sunshine
proc.terminate()
// Wait a moment for it to clean up
proc.waitUntilExit()
}
}
}
// -------------------------------------------------
// Main entry point
// -------------------------------------------------
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.regular) // Shows in Dock, has a menu bar
app.run()
EOF
# -------------------------------------------------
# 3. Compile the Swift launcher
# -------------------------------------------------
echo "🔨 Compiling Native Wrapper (with AppKit)..."
swiftc "$SWIFT_SOURCE" -o "${APP_DIR}/Contents/MacOS/${APP_NAME}"
rm "$SWIFT_SOURCE"
# -------------------------------------------------
# 4. Create Info.plist (includes bundle identifier)
# -------------------------------------------------
cat "${APP_DIR}/Contents/Info.plist"
CFBundleExecutable
${APP_NAME}
CFBundleIdentifier
${BUNDLE_ID}
CFBundleName
${APP_NAME}
CFBundlePackageType
APPL
CFBundleShortVersionString
1.0
CFBundleVersion
1
LSMinimumSystemVersion
13.0
EOF
# -------------------------------------------------
# 5. Make the app executable & clean up
# -------------------------------------------------
chmod +x "${APP_DIR}/Contents/MacOS/${APP_NAME}"
echo "✅ Wrapper app '${APP_DIR}' created successfully!"
echo "🚀 Run it via: open ./${APP_DIR}"
Note: The
Info.plistsnippet above restores the missingCFBundleShortVersionStringline that was cut off in the original post.
3️⃣ Make the script executable
chmod +x sunshine_wrapper.sh
4️⃣ Run the script
./sunshine_wrapper.sh
You should now see a Sunshine app appear in your Applications folder (or the current directory). Opening it will trigger the macOS Screen Recording permission prompt. Grant the permission, and Sunshine will work exactly as before—now with a proper bundle identity that survives macOS 26’s stricter security model.
TL;DR
- macOS 26 no longer shows raw binaries in the Screen‑Recording privacy list.
- Wrap the Sunshine binary in a minimal native
.appbundle. - The provided
sunshine_wrapper.shscript does all the heavy lifting (creates the bundle, compiles a tiny Swift launcher, builds anInfo.plist, and makes it executable). - After running the wrapper, grant screen‑recording permission and your budget‑friendly “Work From Cafe” setup is back in business.
Sunshine Wrapper – Step‑by‑Step Guide
Below is a cleaned‑up version of the original markdown. All code blocks are properly fenced, headings are added for clarity, and the original content is preserved.
1️⃣ Create the Wrapper Script
#!/usr/bin/env bash
# sunshine_wrapper.sh – Wrap Sunshine for macOS 12+ (Monterey/Big Sur)
set -euo pipefail
# -------------------------------------------------
# 1. Variables
# -------------------------------------------------
APP_NAME="Sunshine"
APP_DIR="${HOME}/Applications/${APP_NAME}.app"
BUNDLE_ID="dev.lizardbyte.sunshine.wrapper"
EXECUTABLE="/usr/local/bin/sunshine"
ICON_PATH="/Applications/Sunshine.app/Contents/Resources/AppIcon.icns"
# -------------------------------------------------
# 2. Create the .app bundle structure
# -------------------------------------------------
echo "📁 Creating bundle at ${APP_DIR}..."
mkdir -p "${APP_DIR}/Contents/MacOS"
mkdir -p "${APP_DIR}/Contents/Resources"
# -------------------------------------------------
# 3. Write the Info.plist
# -------------------------------------------------
cat > "${APP_DIR}/Contents/Info.plist"
CFBundleName
Sunshine
CFBundleDisplayName
Sunshine
CFBundleIdentifier
dev.lizardbyte.sunshine.wrapper
CFBundleVersion
2.0
LSMinimumSystemVersion
12.0
NSHighResolutionCapable
CFBundleIconFile
AppIcon
EOF
# -------------------------------------------------
# 4. Copy the executable & icon
# -------------------------------------------------
echo "🔧 Copying executable and icon..."
cp "${EXECUTABLE}" "${APP_DIR}/Contents/MacOS/${APP_NAME}"
chmod +x "${APP_DIR}/Contents/MacOS/${APP_NAME}"
cp "${ICON_PATH}" "${APP_DIR}/Contents/Resources/AppIcon.icns"
# -------------------------------------------------
# 5. Sign the app
# -------------------------------------------------
echo "🔐 Signing the application..."
codesign --force --deep --sign - "${APP_DIR}"
echo "------------------------------------------------"
echo "✅ Success! '${APP_DIR}' created."
echo "------------------------------------------------"
2️⃣ Build the .app Bundle
The script above already creates the bundle, copies the binary and icon, writes a minimal Info.plist, and signs the app. No additional steps are required beyond running the script.
3️⃣ Run and Install
-
Make the script executable
chmod +x sunshine_wrapper.sh -
Execute the script
./sunshine_wrapper.sh -
Move the generated app to
/Applicationsmv "${HOME}/Applications/Sunshine.app" /Applications/ -
Reset Screen‑Capture permissions (if needed)
tccutil reset ScreenCapture dev.lizardbyte.sunshine.wrapper -
Launch the app
When macOS prompts for screen‑capture access, click Allow. The permission will now persist.
🎉 Result
Your portable Mac Mini setup is back in business. You can now stream the Mac Mini to Moonlight or Artemis on your tablet with zero fuss and low latency. If you’re struggling with screen‑sharing tools on newer macOS versions, wrapping them in a native .app makes all the difference.