A macOS menu bar app that shows which local ports are in use — and kills whatever is holding one.
Dev servers and AI coding agents leave listeners behind. Moniport puts them in
the menu bar with the process, its pid and the project directory it was started
from, so freeing port 3000 doesn't mean opening Activity Monitor or remembering
the lsof incantation.
- Sees what's listening. Every TCP port in a configurable range — 3000–9999 by default, or everything you own.
- Names things usefully.
nodebecomesvite, with the working directory it was launched from, so you can tell two dev servers apart. - Kills from the menu bar. Right-click for a graceful
SIGTERM, or force aSIGKILL. Kill All only ever touches what's on screen. - Opens in a click. Click a row for
http://localhost:<port>; copy the URL, pid or full command from the context menu. - Ignores the noise. Hide a port by number, or every port an app opens. Ignored ports leave the list and the count but still register as activity.
- Filters. Search by port, process name or path.
- Stays out of the way. No Dock icon, nothing in Cmd-Tab, follows the system appearance, and can launch at login.
| State | Icon |
|---|---|
| Ports listening, some not ignored | Lit, with the count |
| Ports listening, all ignored | Lit, no count |
| Nothing listening at all | Dimmed |
Moniport is built from source. There's no download or Homebrew cask yet — see Why there's no binary.
Requirements: macOS 14 Sonoma or later, Xcode, and XcodeGen.
brew install xcodegen
git clone https://github.com/iAbadia/Moniport.git
cd Moniport
Scripts/build.sh --runThat builds the app, copies it to /Applications and launches it. The icon
appears in the menu bar — there's no Dock icon, by design. Drop --run to just
build into build/.
To have it start with your Mac, turn on Launch at login in Settings. macOS
only registers login items for apps in a stable location, which is why
--run installs to /Applications rather than running in place.
Distributing a Mac app outside the App Store means signing and notarizing it with a Developer ID certificate, which needs a paid Apple Developer account. Without notarization, macOS refuses to open a downloaded app, and Homebrew quarantines casks by default — so a download would be worse than no download.
Building it yourself sidesteps this entirely: an app you compiled was never quarantined, so Gatekeeper never challenges it. The build signs ad-hoc, with no developer account required.
A signed release and a Homebrew cask are planned. Scripts/release.sh and
Casks/moniport.rb are already written and waiting on the certificate.
Moniport reads listening sockets straight from the kernel with libproc:
proc_listpids to walk processes, proc_pidinfo(PROC_PIDLISTFDS) to walk their
file descriptors, proc_pidfdinfo(PROC_PIDFDSOCKETINFO) to keep the TCP sockets
in LISTEN. That's the same path lsof takes.
It does this rather than shelling out to lsof because the app polls for its
whole lifetime, and spawning a subprocess every few seconds is exactly the sort
of thing that shows up in battery usage. A full scan takes about 6 ms. Reading
the kernel directly also avoids lsof's 9-character truncation of process
names, which is the difference between Code Helper (Plugin) and Code\x20H.
Polling runs at 1 s while the panel is open and 5 s while it's closed; both are configurable.
Only your own processes are visible — reading another user's file descriptors needs elevated privileges, and a dev server you started runs as you.
Tools/portprobe is a CLI over the same code, for checking the scanner against
lsof directly:
cd Tools/portprobe
swift run portprobe # list
swift run portprobe --kill 3000 # terminate whatever holds a port
# should agree, modulo formatting
lsof -nP -a -u $UID -iTCP -sTCP:LISTEN +c 0It's a utility that terminates processes, so it's worth being precise about what it can reach:
- No network access, no telemetry, no analytics. Nothing leaves the machine.
The only URL it handles is the
http://localhost:<port>it hands to your browser when you click a row. - It only sees your own processes. Reading another user's file descriptors needs elevated privileges it doesn't have and doesn't ask for.
- The only thing it writes is your settings, to
UserDefaults. - Killing sends
SIGTERMfirst, escalating toSIGKILLonly if the process ignores it. Before signalling, it re-checks that the pid still belongs to the same process it listed, so a process that exits between refresh and click can't cause an unrelated one to be killed. - It is not sandboxed — reading other processes' file descriptors and signalling them is incompatible with the App Store sandbox.
project.yml is the source of truth — the .xcodeproj is generated and not
checked in, so run Scripts/build.sh (which regenerates it) after adding files.
Open Moniport.xcodeproj afterwards if you'd rather work in Xcode.
| Path | |
|---|---|
Sources/MoniportKit |
Port scanning, process resolution, killing |
Sources/Moniport |
SwiftUI menu bar app |
Tools/portprobe |
CLI harness over MoniportKit |
Design |
Icon artwork and the script that fits it to the icon grid |
Scripts |
Build, icon and release scripts |
Casks |
Homebrew cask, for when there's something to distribute |
| Script | |
|---|---|
Scripts/build.sh [--run] |
Build; --run installs to /Applications and launches |
Scripts/make-icon.sh |
Regenerate the icon from Design/icon-source.png |
Scripts/release.sh <version> |
Archive, sign, notarize, staple, package a DMG |
MoniportKit is a separate module so Tools/portprobe can exercise exactly the
shipping code from a terminal. Tools/portprobe/Sources/MoniportKit is a symlink
to Sources/MoniportKit, not a copy.
Passing --open-settings opens the Settings window at launch, which is otherwise
only reachable by clicking the menu bar panel.
- A signed, notarized release and a Homebrew cask, once there's a Developer ID certificate to sign with
- An App Store build is not possible: reading other processes' file descriptors and signalling them is incompatible with the sandbox
Inspired by Port Monitor, a paid app with the same premise.
MIT — see LICENSE.
