macOS SecureNetwork Suite › macOS 27 ARP
macOS 27: arp -an is empty and netstat shows nothing
Since upgrading to macOS 27, arp -an prints nothing in your scripts, netstat -an lists no connections, and tools that worked before return empty output without an error. Your network is fine. This page explains what is being hidden and from whom, how to confirm it on your Mac, what does not fix it, and how to get the data anyway.
Where the numbers come from. The measurements on this page were taken by the author on one Apple silicon Mac running macOS 27.0 build 26A5388g (Golden Gate public beta 2), when the problem was found on 7 August 2026. The root cause is not confirmed, and no TCC or sandbox denial is logged when it happens. Your build may behave differently, so measure your own Mac before changing anything.
What is happening
On macOS 27, a process forked by a binary that is not signed by Apple reads an empty ARP table and an empty list of Internet sockets.
Homebrew's or MacPorts' bash is the common case, but bash is not the cause: Homebrew's python3 is affected identically, while Apple's /usr/bin/python3 is not. Any non-Apple binary does it. The author also lists xbar, iTerm and kitty as binaries whose children see the blank view.
The restriction is inherited. Once a non-Apple binary has forked, calling /bin/sh, /bin/zsh, /bin/bash or env from inside that process is still affected. You cannot re-exec your way out.
It hides network state, not network traffic. DNS lookups, TCP connections and ICMP all work normally. Nothing is broken except what your tools can see.
What is blanked and what is not
Measured on the same machine at the same moment, from a restricted process and from a clean one:
| Command | Restricted | Clean | Verdict |
|---|---|---|---|
arp -an | 0 | 15 | fully blanked |
netstat -an (TCP/UDP rows) | 0 | 73 | fully blanked |
netstat -rnf inet | 211 | 226 | link-layer rows stripped |
ndp -an | 15 | 19 | partially stripped |
lsof -nP -i | 83 | 83 | unaffected |
ifconfig, scutil, route, networksetup | same | same | unaffected |
| DNS / TCP / ICMP traffic | works | works | unaffected |
Why a quick test says everything is fine
The precise trigger is forking. bash -c 'arp -an' works even under Homebrew bash, because bash runs a lone simple command in place, without forking. A script file, a pipeline, $(...) and process substitution all fork, and all come back empty.
So typing one command at a Homebrew bash prompt, or wrapping it in bash -c, can make you think the problem is somewhere else. It is the forking path your scripts take that is blind.
Check whether your Mac is affected
Compare a clean shell with a restricted, forking one, at the same moment. For the clean side use Apple's zsh in macOS's own Terminal app, not iTerm or kitty, which are themselves non-Apple binaries.
arp -an # Apple zsh: should list hosts
/opt/homebrew/bin/bash -c 'arp -an' # one command, no fork: still works
/opt/homebrew/bin/bash -c 'echo "$(arp -an)"' # $(...) forks: empty if affectedHomebrew on an Intel Mac lives in /usr/local/bin/bash; MacPorts uses /opt/local/bin/bash. If the first two commands list hosts and the third prints nothing, your Mac is affected. If the clean shell's table is empty too, the test proves nothing either way: treat it as inconclusive and try again later.
Then check which interpreter your scripts actually run under. #!/usr/bin/env bash is not clean: it resolves to Homebrew bash whenever /opt/homebrew/bin comes before /bin in your PATH. The same goes for #!/usr/bin/env python3. Check the resolved path, never the shebang text.
Dead ends that look like fixes
- It is not an
ifscopeproblem. Under Homebrew bash,arp -n <ip>says “no entry” whilearp -anlists the host with anifscopequalifier, which makesifscopelook like the culprit. It is not. Run from zsh or/bin/bash,arp -n <ip>works fine. Never testarpbehaviour from Homebrew or MacPorts bash. - Reinstalling bash does nothing.
brew reinstall bashpours the samearm64_tahoe(macOS 26) bottle again. - Building from source does nothing.
--build-from-sourceproduces another ad hoc signed, non-platform binary with the same behaviour. - Re-signing does nothing.
codesign -f -s -changes nothing, so this is not a stale signature. - Changing the shebang to
#!/usr/bin/env bashdoes nothing ifPATHstill finds Homebrew's bash first.
Use lsof instead of netstat
lsof is completely unaffected, and it is the substitute for netstat -an:
lsof -nP -iIt returned the same 83 rows from both sides in the measurements above. For routing questions, route is also unaffected; this is how to read the default gateway safely in a script:
get_gateway_ip() { route -n get default 2>/dev/null | awk '/gateway:/{print $2; exit}' || true; }If you keep a netstat fallback anywhere, make it report “could not look” when it gets nothing. Never let an empty result read as “nothing found”.
Reading the ARP table from an affected script
Nothing inside a restricted process can see the table, so have a clean process read it for you. launchd is Apple's, and a job it starts with an Apple binary is not restricted. The design that works:
- A LaunchAgent runs
/bin/sh, every 15 seconds (StartInterval 15). It must be an Apple binary: that is the whole point. The job runsarp -anand writes the output to a file in your home folder. - Your scripts read the table directly first, and fall back to the file only when the direct read comes back empty. That way they still work when you run them by hand from zsh, and on any machine or macOS release without the restriction.
- Refuse a stale file. The author's limit is 120 seconds. Past that, report that the data is unavailable instead of using it. A stale cache shows the pre-attack gateway MAC address, which would hide exactly the ARP spoofing a network monitor exists to catch.
- Keep your parsing unchanged. Lines in the file look like
? (192.168.0.1) at aa:bb:cc:dd:ee:ff on en0 ifscope [ethernet], so inawk$2is(<ip>)and$4is the MAC address.
A StartInterval job fires briefly and exits, so launchctl print shows it as state = not running between runs. That is correct. Judge it by how fresh the file is, not by whether the process is running.
When Apple lifts the restriction, the direct read starts returning data again and the fallback is never used.
If a launchd script now notifies every 20–30 seconds
It is not repeating a notification. It is crash-looping and restarting. The empty ARP table is what killed it, but set -euo pipefail is what turned one failed lookup into a loop. A line like this:
gateway_mac=$(arp -n "$ip" 2>/dev/null | awk '...')lets pipefail carry arp's exit status 1 into the assignment, and set -e ends the shell, before the main loop, so the script never monitors anything. launchd restarts it under KeepAlive / SuccessfulExit=false, the startup notification fires again, and ThrottleInterval 30 is why you see it every 20–30 seconds.
Harden every helper that calls out to another command, so a passing failure degrades instead of killing the process. The get_gateway_ip function above, ending in || true, is the pattern.
Do not conclude “no error” from an empty stderr log: with 2>/dev/null on the failing command, stderr is empty in this failure mode. Ask launchd instead and look for last exit code and runs:
launchctl print gui/$UID/<label>A packaged version of this workaround
Automated: the macos27-netfix component of macOS-SecureNetwork-Suite packages the design above. Its detect.sh is read-only and measures whether a Homebrew- or MacPorts-rooted process sees an empty ARP table on your Mac (exit 0 affected, 1 not affected, 2 inconclusive); run components/macos27-netfix/detect.sh from /bin/sh, not from Homebrew's or MacPorts' bash. install.sh refuses to install unless that measurement says affected, then adds the com.netwatch.arpcache LaunchAgent and an arp_table() helper. It does not edit your scripts; it lists the ones that need patching. verify.sh checks that the agent is loaded, the cache is fresh and non-empty, and a restricted process can resolve the gateway MAC. The agent removes itself after three consecutive “not affected” verdicts, ten minutes apart, once a macOS or Homebrew update makes the ARP table readable again.
FAQ
Why is arp -an empty on macOS 27?
On macOS 27, a process forked by a binary that Apple did not sign, such as Homebrew or MacPorts bash, reads an empty ARP table, and everything that process starts inherits the restriction. Run arp -an from Apple's zsh or /bin/bash in Terminal and the table is there. The root cause is not confirmed.
Why does netstat -an show no connections on macOS 27?
The same restriction blanks the list of Internet sockets for processes forked from non-Apple binaries. On the author's machine netstat -an showed 0 TCP and UDP rows from the restricted side and 73 from a clean shell at the same moment. Your connections are still there; the listing is what is hidden.
What can I use instead of netstat on macOS 27?
Use lsof -nP -i. On the author's machine it returned the same 83 rows from a restricted process and from a clean one, so it is the substitute for netstat -an.
Does reinstalling or rebuilding Homebrew bash fix the empty ARP table?
No. brew reinstall bash pours the same bottle, a build from source produces another ad hoc signed, non-platform binary with the same behaviour, and re-signing it with codesign changes nothing. Homebrew python3 is affected the same way, so this is not about bash.
Why does bash -c 'arp -an' work but my script returns nothing?
bash runs a lone simple command in place without forking, so that one command escapes the restriction. A script file, a pipeline, command substitution and process substitution all fork, and all come back empty.
Is my Mac's network actually broken?
No. The restriction affects listing network state, not traffic. DNS, TCP and ICMP work normally, and ifconfig, scutil, route and networksetup report the same from a restricted process as from a clean one.