netstat Not Showing Ports on macOS 27: What Changed, What Works
On macOS 27, netstat -an can print no TCP or UDP sockets at all. There is no error and the exit code is 0. The kernel now filters the socket table that netstat reads. To see what is listening, use lsof:
lsof -nP -iTCP -sTCP:LISTEN
That covers your own processes. Put sudo in front for everyone's.
What it looks like
With a dev server listening on port 3000, run netstat through pnpm and count the TCP listeners:
pnpm exec sh -c 'netstat -an -p tcp | grep -c LISTEN'
0
The same netstat command typed straight into a terminal window prints the real count. Through pnpm, without the grep, it prints nothing, not even a header. Plain netstat -an still prints a few hundred UNIX domain sockets, but no tcp4, tcp6 or udp4 lines. lsof finds the server right away:
lsof -nP -iTCP:3000 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 3597 alex 17u IPv6 0x8cc6e94f298c8068 0t0 TCP *:3000 (LISTEN)
Why
netstat gets its list from a sysctl, net.inet.tcp.pcblist_n. On macOS 27 the kernel hands most callers only their own sockets from it. netstat has none of its own, so it gets an empty list and prints an empty list. A comment on dotnet/runtime #133960 describes the change in detail.
netstat is signed by Apple with a private entitlement that exempts it, but the exemption depends on what started the shell. A shell opened directly in Terminal.app, iTerm2, VS Code or Cursor gets the full list. A shell started through pnpm gets nothing. pnpm is ad-hoc signed, as many Homebrew and package-manager executables are, and that seems to be the difference.
Apple hasn't published the rule. The authors of Xymon and everyport each worked one out, and the two disagree. The macOS 27 release notes don't mention it.
What still works
lsof doesn't read the socket table. It looks at each process's open files, so the filter doesn't touch it:
lsof -nP -iTCP -sTCP:LISTEN
Without sudo it shows only processes you own, which has always been the case on macOS. A caddy running as root on ports 80 and 443, for example, is missing.
With sudo it shows every process:
sudo lsof -nP -iTCP -sTCP:LISTEN
nettop sees every user's sockets without sudo. It is normally an interactive monitor, but -L 1 prints one CSV sample and exits:
nettop -L 1 -m tcp -n -J state | awk -F, '$1 !~ /^tcp/ {p=$1} $2 == "Listen" {print p, $1}'
launchd.1 tcp4 127.0.0.1:8021<->*:*
caddy.2223 tcp4 127.0.0.1:443<->*:*
caddy.2223 tcp6 ::1.443<->*.*
rapportd.716 tcp4 *:49178<->*:*
node.3597 tcp6 *.3000<->*.*
That is from the same shell where netstat printed nothing, and it includes root's launchd and caddy. Each line is name.pid and then the socket. Process names are cut to 15 characters, there is no user column, and the CSV format isn't documented. Use -m udp for UDP.
Activity Monitor still has Open Files and Ports in a process's info window, but it shows one process at a time and can't search by port.
If your tool parses netstat
It fails silently. An empty list with exit 0 looks the same as a Mac with nothing listening. .NET's GetActiveTcpListeners() returns zero listeners. pid-port, which fkill :3000 uses, threw "Could not find a process" on every lookup until 2.2.0 added an lsof fallback.
To detect the filter, compare against the socket count, which the kernel still reports:
sysctl -n net.inet.tcp.pcbcount
A non-zero count with zero rows means you were filtered. Then fall back to lsof for the current user's processes, or run nettop for everyone's. pymobiledevice3 and Xymon both switched to nettop.
Portis 1.0 read this sysctl too and showed no ports on macOS 27. Since 1.0.1 it runs nettop there and lists every user's ports again, root's included.