<?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: Jaison Erick</title>
    <description>The latest articles on DEV Community by Jaison Erick (@jaisonerick).</description>
    <link>https://dev.to/jaisonerick</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3893090%2F7ca37161-5262-4293-83ce-2aa40f4b4388.jpeg</url>
      <title>DEV Community: Jaison Erick</title>
      <link>https://dev.to/jaisonerick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jaisonerick"/>
    <language>en</language>
    <item>
      <title>Reading Wi-Fi data from Go on macOS after Apple removed `airport`</title>
      <dc:creator>Jaison Erick</dc:creator>
      <pubDate>Wed, 22 Apr 2026 21:26:10 +0000</pubDate>
      <link>https://dev.to/jaisonerick/reading-wi-fi-data-from-go-on-macos-after-apple-removed-airport-19g</link>
      <guid>https://dev.to/jaisonerick/reading-wi-fi-data-from-go-on-macos-after-apple-removed-airport-19g</guid>
      <description>&lt;p&gt;If you've ever needed your script or your Go program to read the SSID, BSSID, signal strength, or channel of the Wi-Fi network a Mac is on, the last two years have not been kind.&lt;/p&gt;

&lt;p&gt;I'd had a small inventory tool quietly working since 2022. It shelled out to &lt;code&gt;airport -s&lt;/code&gt;, parsed the output, and uploaded a row to a database. Last year, after I upgraded a fleet of MacBooks to Sonoma 14.4, every row was empty. I spent two evenings figuring out why, found a wall I couldn't climb over with a script, and ended up shipping a small library to climb under it. This is the writeup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;airport&lt;/code&gt; — the CLI tool that almost every Mac script used for Wi-Fi info — was removed in macOS Sonoma 14.4. Apple's deprecation notice tells you to use &lt;code&gt;wdutil&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wdutil&lt;/code&gt; requires &lt;code&gt;sudo&lt;/code&gt;, and even with &lt;code&gt;sudo&lt;/code&gt; it gives you this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sudo wdutil info | grep BSSID
BSSID    : &amp;lt;redacted&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same story for SSID and MAC address. They all come back &lt;code&gt;&amp;lt;redacted&amp;gt;&lt;/code&gt;. This was an intentional change starting in macOS 14.5 — Apple has decided that Wi-Fi metadata is location data and is locking it down.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;networksetup -getairportnetwork&lt;/code&gt; still returns the SSID on macOS 13 and 14, but not the BSSID, and Apple disabled it in macOS 15. &lt;code&gt;ioreg&lt;/code&gt; no longer surfaces the values either. &lt;code&gt;system_profiler SPAirPortDataType&lt;/code&gt; gives you signal/noise for the &lt;em&gt;current&lt;/em&gt; connection but not nearby networks, and the BSSID column is empty without the right permissions.&lt;/p&gt;

&lt;p&gt;The only Apple-blessed path is the &lt;strong&gt;CoreWLAN&lt;/strong&gt; framework's &lt;code&gt;scanForNetworks(...)&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CWNetwork&lt;/span&gt; &lt;span class="o"&gt;*&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;scanForNetworksWithName&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;networkName&lt;/span&gt;
                                  &lt;span class="nf"&gt;includeHidden&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;BOOL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;includeHidden&lt;/span&gt;
                                          &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSError&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;_Nullable&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems with that, from a Go program's perspective:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You need to call it from Objective-C / Swift.&lt;/strong&gt; That means either CGo or shelling out to a Swift helper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only returns real BSSIDs to apps that have been granted Location Services permission&lt;/strong&gt;, &lt;em&gt;and&lt;/em&gt; only to apps signed with a stable Developer ID code-signing identity. From a script, an ad-hoc-signed binary, or even a &lt;code&gt;launchd&lt;/code&gt; daemon, it returns &lt;code&gt;nil&lt;/code&gt; BSSIDs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second one is the killer. From the &lt;a href="https://developer.apple.com/forums/thread/718331" rel="noopener noreferrer"&gt;Apple Developer Forums&lt;/a&gt;, Apple's own DTS team:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I recommend that you try this with native code built in to a native app. TCC, the subsystem within macOS that manages the privileges visible in System Preferences &amp;gt; Security &amp;amp; Privacy &amp;gt; Privacy, doesn't work well for scripts.&lt;/p&gt;

&lt;p&gt;For TCC to work reliably the calling code must be signed with a code signing identity that TCC can use to track the code from build to build. Not unsigned. No ad hoc signed.&lt;/p&gt;

&lt;p&gt;Via a &lt;code&gt;launchd&lt;/code&gt; daemon — That's unlikely to work. CoreWLAN checks for the Location privilege and that's hard for a daemon to get.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So: even if I CGo'd CoreWLAN into my Go binary, the binary itself would need a Developer ID code signature for TCC to grant the Location privilege. Go binaries are unsigned by default and change hash on every build, which means TCC will never persistently authorize them.&lt;/p&gt;

&lt;p&gt;This is the wall. There's no flag, no entitlement, no plist key that turns it off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The realization
&lt;/h2&gt;

&lt;p&gt;What TCC &lt;em&gt;does&lt;/em&gt; understand is a separately-signed app bundle with a stable identifier. That's the loophole — not "bypass macOS's controls", but "play by macOS's rules with a different binary".&lt;/p&gt;

&lt;p&gt;The pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your Go binary (unsigned, fine) ships a small companion app bundle inside it.&lt;/li&gt;
&lt;li&gt;The bundle is &lt;strong&gt;signed once&lt;/strong&gt; with a Developer ID Application certificate and notarized via Apple's notary service.&lt;/li&gt;
&lt;li&gt;At runtime, your Go binary extracts the bundle to a temp directory and &lt;code&gt;exec&lt;/code&gt;s &lt;code&gt;open -W&lt;/code&gt; on it.&lt;/li&gt;
&lt;li&gt;The bundle is what asks for the Location Services permission. It has a stable code signature, so TCC happily tracks the authorization across runs.&lt;/li&gt;
&lt;li&gt;The bundle does the CoreWLAN call, serializes the result, and passes it back to the Go side over a small loopback socket.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. Once the user has approved the Location Services prompt once for the bundle, every subsequent &lt;code&gt;Scan()&lt;/code&gt; from your Go program returns real BSSIDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The library
&lt;/h2&gt;

&lt;p&gt;I packaged this pattern as &lt;a href="https://github.com/jaisonerick/macwifi" rel="noopener noreferrer"&gt;&lt;code&gt;macwifi&lt;/code&gt;&lt;/a&gt;. It's a Go library that ships the Developer-ID-signed and notarized helper bundle inside the package via &lt;code&gt;go:embed&lt;/code&gt;. Calling code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/jaisonerick/macwifi"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;nets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;macwifi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;nets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%-32s %s  %d dBm  ch %d  %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SSID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BSSID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RSSI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Security&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the user approves the macOS Location Services prompt the first time, that prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Office Wi-Fi                     aa:bb:cc:dd:ee:ff   -52 dBm  ch 149  WPA2
Guest                            11:22:33:44:55:66   -71 dBm  ch  36  WPA2
Conference Room                  77:88:99:aa:bb:cc   -58 dBm  ch 100  WPA3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Network&lt;/code&gt; struct carries everything CoreWLAN exposes that a Go developer is likely to want: SSID, BSSID, RSSI, noise floor, channel number, channel band (2.4 / 5 / 6 GHz), channel width, security mode (open / WEP / WPA / WPA2 / WPA3 / Enterprise / OWE), PHY mode, plus &lt;code&gt;Current&lt;/code&gt; and &lt;code&gt;Saved&lt;/code&gt; flags.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;macwifi.Password(ctx, ssid)&lt;/code&gt; for reading saved Wi-Fi passwords from the System keychain. That uses &lt;code&gt;CWKeychainFindWiFiPassword&lt;/code&gt; and triggers the macOS Keychain &lt;strong&gt;Allow&lt;/strong&gt; / &lt;strong&gt;Deny&lt;/strong&gt; dialog. (The legacy &lt;em&gt;Always Allow&lt;/em&gt; button is gone in current macOS, so the prompt fires every time you call it. That's deliberate on Apple's part.)&lt;/p&gt;

&lt;p&gt;If you'd rather not write Go and just want a working &lt;code&gt;airport -s&lt;/code&gt; replacement on the terminal, the same package powers a small CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;jaisonerick/tap/macwifi-cli

macwifi-cli scan
macwifi-cli info             &lt;span class="c"&gt;# current network only&lt;/span&gt;
macwifi-cli password &lt;span class="s2"&gt;"MyWiFi"&lt;/span&gt;
macwifi-cli scan &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.[] | select(.rssi &amp;gt; -65)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you give up
&lt;/h2&gt;

&lt;p&gt;This isn't a free workaround. Some honest scope notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS 13+ on Apple Silicon only.&lt;/strong&gt; Intel Macs aren't supported. No plans to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doesn't work from a system-wide &lt;code&gt;launchd&lt;/code&gt; daemon.&lt;/strong&gt; CoreWLAN's Location Services check is per-user-session, so the helper bundle needs at least one user logged in. Works fine from a &lt;code&gt;launchd&lt;/code&gt; &lt;em&gt;agent&lt;/em&gt; or any program a user runs themselves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Location Services prompt always fires once.&lt;/strong&gt; There's no API to suppress it; that's the entire point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Keychain prompt fires every time&lt;/strong&gt; you call &lt;code&gt;Password()&lt;/code&gt; for a given SSID, because &lt;em&gt;Always Allow&lt;/em&gt; no longer exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a packet capture library.&lt;/strong&gt; For sniffing, Wireshark and Wireless Diagnostics' Sniffer Mode are still the right tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The macOS-vs-power-users story keeps repeating itself: Apple closes a door, the workaround that kept everyone happy for 15 years stops working, and there's exactly one path that's still open if you're willing to put a small signed binary in your build pipeline. It's worth knowing the &lt;em&gt;shape&lt;/em&gt; of that path, even if you never need it yourself, because the same pattern shows up everywhere TCC is involved (Full Disk Access, Accessibility, Screen Recording).&lt;/p&gt;

&lt;p&gt;For Wi-Fi specifically, the pattern is now baked into a library, so the next person who needs to read a BSSID from a Go program on a fleet of MacBooks doesn't have to spend two evenings rediscovering the constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/jaisonerick/macwifi" rel="noopener noreferrer"&gt;https://github.com/jaisonerick/macwifi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://jaisonerick.github.io/macwifi/" rel="noopener noreferrer"&gt;https://jaisonerick.github.io/macwifi/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How it works (architecture deep-dive): &lt;a href="https://jaisonerick.github.io/macwifi/how-it-works" rel="noopener noreferrer"&gt;https://jaisonerick.github.io/macwifi/how-it-works&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;pkg.go.dev: &lt;a href="https://pkg.go.dev/github.com/jaisonerick/macwifi" rel="noopener noreferrer"&gt;https://pkg.go.dev/github.com/jaisonerick/macwifi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;macwifi-cli (terminal version): &lt;a href="https://github.com/jaisonerick/macwifi-cli" rel="noopener noreferrer"&gt;https://github.com/jaisonerick/macwifi-cli&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've hit the same wall and want to compare notes — or if you're on Apple's side of this and have a cleaner path I missed — the issue tracker is open.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>go</category>
      <category>networking</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
