How I Fixed Sunshine Screen Sharing on macOS Tahoe (The Native Wrapper Method)

Published: (December 31, 2025 at 10:38 PM EST)
6 min read
Source: Dev.to

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 workWhat 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:

  1. Packages the Sunshine binary inside a proper app bundle.
  2. Gives the bundle a stable identifier (dev.lizardbyte.sunshine.wrapper).
  3. Triggers the macOS screen‑recording permission prompt.
  4. 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.plist snippet above restores the missing CFBundleShortVersionString line 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 .app bundle.
  • The provided sunshine_wrapper.sh script does all the heavy lifting (creates the bundle, compiles a tiny Swift launcher, builds an Info.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

  1. Make the script executable

    chmod +x sunshine_wrapper.sh
  2. Execute the script

    ./sunshine_wrapper.sh
  3. Move the generated app to /Applications

    mv "${HOME}/Applications/Sunshine.app" /Applications/
  4. Reset Screen‑Capture permissions (if needed)

    tccutil reset ScreenCapture dev.lizardbyte.sunshine.wrapper
  5. 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.

Back to Blog

Related posts

Read more »

The RGB LED Sidequest 💡

markdown !Jennifer Davishttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...

Mendex: Why I Build

Introduction Hello everyone. Today I want to share who I am, what I'm building, and why. Early Career and Burnout I started my career as a developer 17 years a...