Add Sparkle auto-update support to a native macOS app. Use this skill whenever the user wants to add auto-updates, implement Sparkle, set up an appcast feed, add "Check for Updates" to their app, or integrate automatic update checking. Also trigger when the user mentions Sparkle, appcast, SUFeedURL, EdDSA signing, or update notifications. This covers the full setup: adding the Sparkle SPM dependency, creating the UpdaterManager singleton, wiring it into the app delegate and UI, configuring Info.plist, and generating the initial appcast.xml.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
This skill adds Sparkle auto-update support to a native macOS app. Sparkle is the standard open-source framework for macOS app updates outside the Mac App Store.
The implementation has 4 parts:
SPUStandardUpdaterControllerSUFeedURL and SUPublicEDKeyIn Xcode: File > Add Package Dependencies > enter:
https://github.com/sparkle-project/SparkleUse the "Up to Next Major Version" rule with 2.0.0. Add the Sparkle framework to your app target.
Or add it to Package.swift if your project uses one:
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")Copy references/UpdaterManager.swift into your project. This is a singleton that:
SPUStandardUpdaterController early (before applicationDidFinishLaunching returns)canCheckForUpdates for UI bindingautomaticallyChecksForUpdates toggle.regular activation policy before showing the update windowThe key design decisions in this file:
startingUpdater: false in the initializer, then calling start() explicitly in applicationDidFinishLaunching. This gives you control over timing.start() and checkForUpdates(). Sparkle should never run in debug builds -- it would try to update your debug app with a release build.ObservableObject with @Published (not @Observable) because we need the Combine publisher(for:) bridge from Sparkle's KVO.Add these keys to your app's Info.plist:
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/OWNER/REPO/main/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>YOUR_PUBLIC_EDDSA_KEY_HERE</string>
<key>SUEnableInstallerLauncherService</key>
<true/>Sparkle uses EdDSA (Ed25519) signing. Generate a keypair:
# Find generate_keys in your DerivedData after building the project with Sparkle
find ~/Library/Developer/Xcode/DerivedData -name "generate_keys" -type f 2>/dev/null | head -1Run it:
/path/to/generate_keysThis prints the public key and stores the private key in your Keychain. Put the public key in SUPublicEDKey in Info.plist. The private key stays in Keychain and is used by sign_update during release.
final class AppDelegate: NSObject, NSApplicationDelegate {
private let updaterManager = UpdaterManager.shared
func applicationDidFinishLaunching(_ notification: Notification) {
updaterManager.start()
}
}The UpdaterManager.shared property must be accessed early so the SPUStandardUpdaterController is created before the app finishes launching. Referencing it in the AppDelegate property ensures this.
struct AboutSettingsPane: View {
@ObservedObject private var updaterManager = UpdaterManager.shared
var body: some View {
Form {
Section("Updates") {
Toggle(isOn: Binding(
get: { updaterManager.automaticallyChecksForUpdates },
set: { updaterManager.automaticallyChecksForUpdates = $0 }
)) {
Text("Automatically check for updates")
}
Button("Check for Updates...") {
updaterManager.checkForUpdates()
}
.disabled(!updaterManager.canCheckForUpdates)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
}
}Button {
updaterManager.checkForUpdates()
} label: {
Label("Check for Updates...", systemImage: "arrow.down.circle")
}
.disabled(!updaterManager.canCheckForUpdates)Create an appcast.xml at the root of your repo. It starts empty and gets populated by your release process:
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>YourApp</title>
<description>Most recent changes for YourApp.</description>
<language>en</language>
</channel>
</rss>Host this file on GitHub (raw URL) or any static file host. The URL must match SUFeedURL in Info.plist.
If your app runs as .accessory (no Dock icon), Sparkle's update window won't appear unless you temporarily switch to .regular. The reference UpdaterManager handles this in checkForUpdates():
func checkForUpdates() {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
controller.checkForUpdates(nil)
}The app reverts to .accessory when the update window closes (handled by your existing activation policy manager).
If your app uses Hardened Runtime (required for notarization), no special Sparkle entitlements are needed. Sparkle 2.x works with the standard hardened runtime configuration.
Each release in the appcast looks like this (for reference when building release tooling):
<item>
<title>Version 1.2 (Build 5)</title>
<pubDate>Mon, 26 May 2026 12:00:00 +0000</pubDate>
<sparkle:version>5</sparkle:version>
<sparkle:shortVersionString>1.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<description><![CDATA[<ul><li>New feature</li><li>Bug fix</li></ul>]]></description>
<enclosure url="https://github.com/OWNER/REPO/releases/download/v1.2/YourApp.dmg"
type="application/octet-stream"
sparkle:edSignature="BASE64_EDDSA_SIGNATURE"
length="FILE_SIZE_BYTES" />
</item>sparkle:version = CFBundleVersion (build number)sparkle:shortVersionString = CFBundleShortVersionString (marketing version)sparkle:edSignature = output of sign_update YourApp.dmglength = file size in bytesfreekmurze/dotfiles
fe89482
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.