<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Graeme Chard</title>
    <description>The latest articles on DEV Community by Graeme Chard (@graeme_chard_2bf31b0e24f1).</description>
    <link>https://dev.to/graeme_chard_2bf31b0e24f1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4016013%2F0a12369a-2a15-409d-8b27-aef1fe77fdf1.jpg</url>
      <title>DEV Community: Graeme Chard</title>
      <link>https://dev.to/graeme_chard_2bf31b0e24f1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/graeme_chard_2bf31b0e24f1"/>
    <language>en</language>
    <item>
      <title>Signed, notarized, and silently unauthorized: the ShazamKit provisioning trap for Developer ID Mac apps</title>
      <dc:creator>Graeme Chard</dc:creator>
      <pubDate>Sat, 05 Sep 2026 15:00:23 +0000</pubDate>
      <link>https://dev.to/graeme_chard_2bf31b0e24f1/signed-notarized-and-silently-unauthorized-the-shazamkit-provisioning-trap-for-developer-id-mac-18p</link>
      <guid>https://dev.to/graeme_chard_2bf31b0e24f1/signed-notarized-and-silently-unauthorized-the-shazamkit-provisioning-trap-for-developer-id-mac-18p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://feeds.bar/engineering/shazamkit-provisioning" rel="noopener noreferrer"&gt;feeds.bar&lt;/a&gt;, August 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F61zvmvwullkns0e01kc2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F61zvmvwullkns0e01kc2.webp" alt="The FeedsBar ticker in music mode, showing now-playing artwork and music-lane items" width="800" height="60"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The feature at stake: the ticker re-tunes to whatever is playing, including music it hears in the room.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How FeedsBar is put together
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Then we cut the release build. Archive, export, notarize, staple, every check green. &lt;code&gt;spctl&lt;/code&gt; accepted it. &lt;code&gt;stapler validate&lt;/code&gt; 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.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the shipped build was different
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;embedded provisioning profile&lt;/strong&gt; proving it.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same app, two exports.&lt;/strong&gt; 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 &lt;code&gt;-allowProvisioningUpdates&lt;/code&gt;: 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.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix, in two flags
&lt;/h2&gt;

&lt;p&gt;The release script now archives and exports with automatic signing, and passes &lt;code&gt;-allowProvisioningUpdates&lt;/code&gt; in both places. That lets xcodebuild mint and embed the profile ("Mac Team Direct Provisioning Profile", expiry 2044) during the developer-id export:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcodebuild &lt;span class="nt"&gt;-project&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-scheme&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCHEME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-configuration&lt;/span&gt; Release &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-archivePath&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ARCHIVE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-allowProvisioningUpdates&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;CODE_SIGN_STYLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Automatic &lt;span class="nv"&gt;CODE_SIGN_IDENTITY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Apple Development"&lt;/span&gt; archive

&lt;span class="c"&gt;# ExportOptions.plist&lt;/span&gt;
&lt;span class="c"&gt;#   method:        developer-id&lt;/span&gt;
&lt;span class="c"&gt;#   signingStyle:  automatic&lt;/span&gt;

xcodebuild &lt;span class="nt"&gt;-exportArchive&lt;/span&gt; &lt;span class="nt"&gt;-archivePath&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ARCHIVE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-allowProvisioningUpdates&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-exportOptionsPlist&lt;/span&gt; ExportOptions.plist &lt;span class="nt"&gt;-exportPath&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXPORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="s2"&gt;"FeedsBar.app/Contents/embedded.provisionprofile"&lt;/span&gt;
security cms &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"FeedsBar.app/Contents/embedded.provisionprofile"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | plutil &lt;span class="nt"&gt;-extract&lt;/span&gt; Name raw &lt;span class="nt"&gt;-o&lt;/span&gt; - -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decoy bug that cost the most time
&lt;/h2&gt;

&lt;p&gt;While chasing this we spent hours convinced the app was not logging at all. &lt;code&gt;log stream&lt;/code&gt; and &lt;code&gt;log show&lt;/code&gt; printed nothing from our subsystem. The explanation is embarrassing in the way the best traps are: &lt;code&gt;log&lt;/code&gt; is a zsh builtin. Inside a zsh-invoked script, &lt;code&gt;log show&lt;/code&gt; 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 &lt;code&gt;/usr/bin/log&lt;/code&gt;, always.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Signature valid, notarization passed, staple verified: none of these say anything about App Service authorization. That lives in the embedded profile.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;In shell scripts on macOS, it is &lt;code&gt;/usr/bin/log&lt;/code&gt;. The builtin will waste your afternoon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FeedsBar is built by a tiny team in Dublin. If you want to see the feature this post paid for, the &lt;a href="https://feeds.bar/" rel="noopener noreferrer"&gt;home page&lt;/a&gt; shows it live, and the &lt;a href="https://feeds.bar/press/" rel="noopener noreferrer"&gt;press kit&lt;/a&gt; has the rest.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>xcode</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
