A macOS menu bar app that shows the local servers you are running and lets you kill any of them with one click.
If you run a lot of development servers, it is easy to lose track of what is on
:3000 versus :8080, and freeing a stuck port usually means running
lsof -i :3000 and kill <pid> by hand. Portly puts that in your menu bar: a
glanceable list of your local servers, each with a description and a kill button.
- Your servers, not the system's. By default Portly hides macOS background
services (Control Center / AirPlay,
rapportd,ARDAgent, and similar) and ephemeral internal sockets, so the list is only the things you started. - Readable descriptions. Each port is labelled from your own custom name, a built-in table of well-known ports (PostgreSQL, Redis, Vite, and more), or the owning process name, in that order.
- One-click kill. Click to terminate gracefully (
SIGTERM); hold Option to force-kill (SIGKILL). No confirmation dialog, no terminal. - Custom labels. Right-click a row to label a port (for example, name
:3000"Frontend"). Labels persist across launches. - Exposure indicator. Ports bound to all interfaces (reachable from your network, not just loopback) are flagged.
- Lightweight and native. A single SwiftUI
MenuBarExtra, no Dock icon, no runtime dependencies. The built app is well under one megabyte.
Portly enumerates listening TCP sockets with lsof and groups them by port:
lsof -nP -iTCP -sTCP:LISTEN +c0 -FpcnL
The -F field mode gives machine-readable output that is parsed into one entry
per port (IPv4 and IPv6 listeners for the same port are merged).
On macOS every listener typically runs under your user account, including Apple's own daemons, so ownership alone cannot separate them. Portly uses two signals instead:
- Port range. Listeners in the IANA dynamic/ephemeral range (
>= 49152) are hidden. These are short-lived internal IPC sockets, not servers you open in a browser. - Process denylist. A curated list of Apple service processes
(
ControlCenter,rapportd,ARDAgent,sharingd, and others) is hidden.
A "Show system and background services" toggle in the options menu reveals the full list when you want it.
Kill is a direct kill(2) system call, not a shell-out. The result is mapped to
a clear outcome, including EPERM (a root-owned port shows "Needs admin" rather
than failing silently) and ESRCH (already gone).
- macOS 14 (Sonoma) or later
- Swift 6 toolchain (Xcode 16+ or the Swift toolchain command line tools)
Portly is intentionally not sandboxed, because the App Sandbox blocks both
spawning lsof and signalling other processes.
git clone https://github.com/zabrodsk/portly.git
cd portly
./build.sh # release build, assembles and ad-hoc signs Portly.app
open Portly.appUse ./build.sh --debug for a faster debug build while iterating.
swift testThe suite covers the lsof parser (IPv4/IPv6 grouping, address splitting), the
"local host" filter, description resolution priority, and the real kill(2)
paths, including the permission-denied and no-such-process cases.
Package.swift SwiftPM manifest (executable + test target)
build.sh Build, assemble, and ad-hoc sign Portly.app
Resources/Info.plist LSUIElement (menu-bar-only) bundle metadata
Sources/Portly/
PortlyApp.swift App entry point, MenuBarExtra scene
AppModel.swift Observable state, refresh timer, kill actions
PortScanner.swift lsof invocation, parsing, and filtering
ProcessKiller.swift kill(2) wrapper with errno mapping
PortDescriptions.swift Well-known-port table and title resolution
LabelStore.swift Custom labels persisted in UserDefaults
Models.swift PortEntry value type
Views/
PortsListView.swift Popover: header, list, options, footer
PortRowView.swift A single port row and its kill button
Tests/PortlyTests/ XCTest suite
build.sh ad-hoc signs the app so it runs on the machine that built it. To
distribute a signed and notarized build, package Portly.app into a DMG and run
it through Apple's notarization with your Developer ID.
