See what's listening. Reclaim the port. One keypress.
An interactive TCP port manager for your terminal — with managed
kubectl port-forward sessions and Cloudflare Tunnel visibility.
Install • Features • Usage • Configuration • How it works • Development
You run bun dev and ports 3000, 3001, and 8484 are "in use". By what?
An orphaned SSH tunnel? Yesterday's dev server? ports answers in one
screen and clears it in one keypress.
# Homebrew (macOS / Linux)
brew install dupe-com/tap/ports-cli
# Go
go install github.com/dupe-com/ports-cli/cmd/ports@latest
# curl (downloads the right release binary to /usr/local/bin)
curl -fsSL https://raw.githubusercontent.com/dupe-com/ports-cli/main/install.sh | shOr grab a binary from Releases.
Every method installs the binary as ports.
- ⚡ Live table of every listening TCP port — process, owner, uptime, CPU/mem, bind address. Auto-refreshes (configurable, pausable).
- 🎯 Focus mode by default — the list is grouped by category (dev servers
first), and system daemons / unclassified ports are folded away until you
scroll past the end or press
a. The ports you're hunting for, not 30 rows ofrapportd. - 🔪 One-keypress kill — graceful
SIGTERMwith a grace window,Fto escalate toSIGKILL. Multi-select withspace, orKto clear everything visible at once. - 🔍 Fuzzy filter (
/) across port, process name, user, and full command line — plus a category filter (c) that cycles dev / web / db / messaging / tunnel / system. Filters always search everything, folded or not. - 🏷️ Smart categorization — postgres on a weird port is still a
DB; rules match the process first, well-known ports second. SSH forwards are carriers: anssh -Lof your dev server shows asDEV (SSH)in a distinct tint, and unrecognized forwarded ports get their own always-visibleTUN · tunnels & forwardsgroup. - 🌳 Tree view (
t) — group ports by owning process instead. - ★ Favorites — pin the ports you care about to the top of their group.
- 👁️ Watched ports — get a desktop notification when a port starts or stops listening ("tell me when the dev server is actually up").
- 📋 Copy (
y) — putlocalhost:PORTon the clipboard. - ☸️ Managed
kubectl port-forwardsessions — create from a form, watch status live, view logs, and let them auto-reconnect with backoff when the connection drops. Save specs (s) to relaunch them in one keypress next session. No more dead forwards after a pod restart. - ☁️ Cloudflare Tunnel visibility — see every running
cloudflared, named or quick, with its origin and config. (Tunnels dial out, so they never show up in a port scan — this tab is how you see them.) - 🤖 Scriptable — every feature has a flag-driven subcommand with
--jsonoutput where it matters.
ports| Key | Action |
|---|---|
1 2 3 / ←→ / tab |
switch tabs (Ports / kubectl / cloudflared) |
↑↓ j k |
move · g/G top/bottom |
a |
reveal/fold system & misc ports (↓ past the end also reveals) |
/ |
fuzzy filter |
c |
cycle category filter |
space |
multi-select |
enter / x |
kill — confirm with y (graceful) or F (force) |
K |
kill everything visible (with confirmation) |
y |
copy localhost:PORT to clipboard |
t |
tree view — group ports by owning process |
d |
hide/show detail pane (full cmdline, all ports held by the pid) |
f / w |
toggle favorite ★ / watched 👁 |
r / p |
refresh now / pause auto-refresh |
n / s / D |
(kubectl tab) new forward / save spec / delete saved spec |
esc |
back out one layer: filter → fold → quit |
? |
help |
ports list # table of all listeners
ports list --json # same, as JSON
ports list --category db # only databases
ports list --filter node # name/cmdline substring
ports kill 3000 # kill whatever holds :3000 (confirms)
ports kill 3000 8080 --yes # no confirmation
ports kill node --force # by name, SIGTERM → SIGKILL
ports watch 3000 # notify when :3000 starts/stops listening
ports watch 3000 5432 --interval 1s
ports fwd svc/api 8080:80 # kubectl port-forward that auto-reconnects
ports fwd pod/web-0 3000 -n staging --context prod~/.config/ports-cli/config.toml (created on first favorite/watch; all keys
optional):
refresh_interval = "2s" # TUI auto-refresh; "0" disables
grace_period = "1500ms" # SIGTERM → SIGKILL window
notify = true # desktop notifications
favorites = [3000, 5432]
watched = [8080]
# saved kubectl port-forward specs (press s on a running session to add)
[[forwards]]
target = "svc/api"
ports = ["8080:80"]
namespace = "staging"Override the location with $PORTS_CLI_CONFIG.
- Discovery —
lsoffield-output on macOS (the most reliable unprivileged source there), gopsutil's connection table elsewhere. You see the processes your user can see; run withsudoto see everything. - Categorization — process-name rules first, well-known-port rules second.
ssh/autosshare carriers: their name says nothing about what the port is for, so the port rule decides (3000forwarded over ssh is stillDEV), and unmatched carried ports becomeTUNinstead of being mistaken for system noise. - Kill —
SIGTERM, a grace window for clean shutdown, then opt-inSIGKILLfor survivors. Multi-port processes are signalled once. - Forward sessions — children of the TUI/CLI process, supervised with
exponential backoff (1s → 30s cap), reset on successful reconnect. They end
when
portsdoes — no daemons, no state files. - Tunnels — read-only detection of
cloudflaredprocesses via the process table.
make build # build ./bin/ports
make test # go test ./...
make lint # golangci-lint (or go vet fallback)
make snapshot # goreleaser snapshot buildSee CONTRIBUTING.md.
ports-cli grew out of the port-killer script in
agent-mac-ops — a toolkit for
operating an always-on Mac remotely (agent-driven ops, native terminal
sessions, browser/clipboard handoff, port forwarding). If you're clearing
ports on a remote dev box over ssh, you probably want both: agent-mac-ops
creates the forwards, ports-cli is how you see them (DEV (SSH) rows).
MIT © Dupe, Inc.

