Originally published on feeds.bar, August 2026.
Our ShazamKit feature worked in every debug build and failed in every shipped one. The signature was fine. The staple was fine. The missing piece was a file most Mac developers have never had to think about.

The feature at stake: the ticker re-tunes to whatever is playing, including music it hears in the room.
How FeedsBar is put together
Context first, because the bug only makes sense against the architecture. FeedsBar is a news ticker for the Mac desktop, and it is a four-repo product with one design rule: the client stays thin. All the intelligence lives server-side.
Workers on Cloud Run ingest roughly nine hundred feeds. They run the scoring pipeline that ranks them, and the methodology is published. Everything lands in Postgres.
A set of Netlify functions serves it back out: a manifest, the Signal 500 catalogue, the now-playing music lanes, licensing. The Swift client renders and handles interaction, and that is all it does. It computes no scores, builds no favicon URLs, decides nothing editorial.
Even curated sets and topic pillars arrive as data in the manifest. Shipping a new set touches zero client code. The discipline has a sharp consequence: a client rollback fixes nothing, because the behavior you are rolling back never lived in the binary. The website is the fourth repo, a Vite site that bakes 478 routes from the same API so crawlers see real HTML.
There is exactly one place where the thin client does something heavy on its own: listening to the room. ShazamKit matches audio signatures on-device, and the audio never leaves Apple's framework. None of it can move server-side. Which is how a proudly boring client walked into the least boring signing problem we have hit.
The setup
FeedsBar is sold outside the App Store as a notarized Developer ID app. Version 1.2.0 added the listening mode: with permission, the app identifies music playing in the room via ShazamKit and re-tunes the ticker to that artist. In development it worked every time. Xcode, hit run, hum something, matched.
Then we cut the release build. Archive, export, notarize, staple, every check green. spctl accepted it. stapler validate passed. And every single ShazamKit match request came back as a 202 with an AMS 401 buried in the logs. Not one match, ever, from the exported app. Same source, same Mac, same microphone.
Why the shipped build was different
Mac developers learn early that entitlements ride in the code signature, and that for most capabilities that is the whole story. Sandbox, hardened runtime, microphone access: all asserted by the signature, all working in our exported build.
ShazamKit is not in that category. It is an App Service, granted to an App ID in the developer portal. macOS honors the grant only when the app carries an embedded provisioning profile proving it.
iOS developers never notice, because every iOS build embeds a profile. Direct-distribution Mac apps are the odd case. A Developer ID app needs no profile unless it uses one of these services. So the standard manual export produces a perfectly valid app with no profile inside, and the service quietly refuses it at runtime.
Debug builds worked because Xcode's run pipeline provisions automatically. The failure existed only in the artifact we shipped, which is the worst place for a failure to live.
The same app, two exports. Manual signing with no profile: archive, export signs clean, notarize and staple pass, no embedded provisioning profile, and every ShazamKit match returns 202 with an AMS 401. Automatic signing with -allowProvisioningUpdates: archive, developer-id export with automatic signing style, profile minted and embedded, "Mac Team Direct Provisioning Profile" inside, room audio identified on the first try.
The fix, in two flags
The release script now archives and exports with automatic signing, and passes -allowProvisioningUpdates in both places. That lets xcodebuild mint and embed the profile ("Mac Team Direct Provisioning Profile", expiry 2044) during the developer-id export:
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \
-archivePath "$ARCHIVE" -allowProvisioningUpdates \
CODE_SIGN_STYLE=Automatic CODE_SIGN_IDENTITY="Apple Development" archive
# ExportOptions.plist
# method: developer-id
# signingStyle: automatic
xcodebuild -exportArchive -archivePath "$ARCHIVE" -allowProvisioningUpdates \
-exportOptionsPlist ExportOptions.plist -exportPath "$EXPORT"
The archive signs with Apple Development; the export re-signs with the Developer ID certificate and embeds the profile. Verify it is in the product before trusting anything:
ls "FeedsBar.app/Contents/embedded.provisionprofile"
security cms -D -i "FeedsBar.app/Contents/embedded.provisionprofile" \
| plutil -extract Name raw -o - -
One wrinkle worth keeping. Our project file still pins Release to Manual signing with the Developer ID identity, because an Xcode holding only a development cert will otherwise "helpfully" rewrite the config. The script overrides at build time. Config files record intent; the pipeline enforces reality.
The decoy bug that cost the most time
While chasing this we spent hours convinced the app was not logging at all. log stream and log show printed nothing from our subsystem. The explanation is embarrassing in the way the best traps are: log is a zsh builtin. Inside a zsh-invoked script, log show runs the shell's builtin, not Apple's log tool, and either prints nothing or complains about arguments. The app had been logging correctly the whole time. Our scripts now say /usr/bin/log, always.
Takeaways
- Signature valid, notarization passed, staple verified: none of these say anything about App Service authorization. That lives in the embedded profile.
- If a capability works in debug and dies in the exported build, ask what Xcode's run pipeline gives you that your export does not. Provisioning is the usual answer.
- Verify the exported artifact does the actual thing, on device, before shipping. Our release checklist now includes a live room-audio match from the stapled build.
- In shell scripts on macOS, it is
/usr/bin/log. The builtin will waste your afternoon.
FeedsBar is built by a tiny team in Dublin. If you want to see the feature this post paid for, the home page shows it live, and the press kit has the rest.
Top comments (0)