Does anyone else end up with a pile of things running on localhost and forget what half of them are?
A dev server, two APIs, a database, a container, a tunnel. You started some. Claude, Cursor and Codex started the rest, days ago, in a window you have since closed. Nothing on the machine can tell you which is which, so none of it gets turned off and it all comes back tomorrow.
lsof can tell you a PID owns port 8000. It cannot tell you
that an agent opened it on Tuesday while working on the billing retries, in a project you
have not touched since, and that nothing has connected to it in six days. That second answer
is the one that lets you close it.
What is listening?
Every TCP and UDP service, one row each, named from the command and the project rather than from a port number. A row is a service on a port, not a port: two processes can hold the same number with different reach, and collapsing them loses the one that matters.
| you ask | lsof says | portlist says |
|---|---|---|
| what is on :8787? | Python, pid 96798 | a static file server, started by a Claude Code session, in ~/code/data-export |
| who started it? | nothing | that session, and whether it has since exited |
| can anyone reach it? | 0.0.0.0, a bind string | what answered when it connected to this machine's real address |
| is it still needed? | how long it has been up | how long it has been unused, measured over time |
| is it safe to kill? | nothing | what depends on it, and the command to stop it |
Who started it? Usually an AI.
Process ancestry first, environment second, and a launch record last. On a machine where Claude Code, Cursor, Codex or Copilot is doing the typing, most of what is listening was started by something other than you, and the useful question stops being which process and becomes which session, in which project, and does it still matter. Six AI coding tools are read from the store each already writes, and nothing is installed into any of them.
PORTLIST devbox 16 listening 2 off-box 2 need attention O 02:35:15 0 Dashboard 1 Services 2 Exposed 3 Attention 4 Leftovers ▌5 Agents 6 Containers 7 Sessions 8 System 9 Graph ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── PORT SERVICE PROJECT REACHABLE RISK STARTED BY a Claude Code session - session has exited - 14 services :8000 Python http.server analytics Localhost only 12 Info Claude Code 21d :8078 sink.py payments Localhost only 6 Info Claude Code 29d :8422 store-helper.mjs data-export Localhost only 6 Info Claude Code 2d :8787 Python http.server data-export All interfaces 71 High Claude Code 6d :8787 FastAPI / Uvicorn scanner Localhost only 12 Info Claude Code 23d :8807 Python http.server data-export All interfaces 71 High Claude Code 3d :42065 Bun portlist Localhost only 0 Info Claude Code 4h :48744 Bun shopfront Localhost only 0 Info Claude Code 25h :55302 Bun portlist Localhost only 6 Info Claude Code 4h :56050 Bun ~/Downloads Localhost only 6 Info Claude Code 3h :56609 Bun scanner Localhost only 6 Info Claude Code 25d :57244 Bun data-export Localhost only 6 Info Claude Code 6d :59195 Bun ~/Downloads Localhost only 0 Info Claude Code 3h :61535 Bun shopfront Localhost only 6 Info Claude Code 25h a terminal - still running - 1 service :7337 shopfront shopfront Localhost only 6 Info terminal 10h launchd - still running - 1 service :9050 Tor /opt/homebrew Localhost only 12 Info launchd 24d ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── j/k move h/l pane tab view enter detail O open / search f free port V vibe q quit updated 1s ago
Which of those windows can you close?
Twelve AI coding sessions open, none of them closed, and no way to tell them apart from the outside. You have wondered which of them is still doing something, which left a server behind, and which you can safely close. Every one is a transcript on disk that Claude, Codex, Copilot, Cursor or Gemini already wrote. Reading those is the difference between a list of processes and a list of decisions.
- Open now, then left on disk. A session with a process behind it is one you can act on. One without is a transcript nobody is holding. Mixing them in a single list ordered by recency buried the only rows that matter.
- What it was actually about. The title its own tool generated, and the first prompt you typed to start it, which is usually the thing you recognise when the title is not enough.
- How much it is carrying. The tokens on the last turn, taken from the usage the transcript records, next to a bar comparing it with the biggest session on this machine. Not a percentage of a context window: nothing on disk says what that model's limit is, so no number here pretends to.
- How long since anyone touched it. Five hours, six days, a fortnight. Stale is the axis you actually close along.
- Which account is paying. One line per tool for the plan it is signed in under, because that is the answer to which one is burning quota.
- It prints the command; you run it. And when four
agents share a directory it says so rather than picking one, because a
killthat might close the wrong window is worse than no command at all.
PORTLIST devbox 16 listening 2 off-box 2 need attention 04:18:52 0 1 2 3 4 5 6 ▌7 Sessions 8 9 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── TOOL WHAT IT WAS ABOUT PROJECT CONTEXT VS BIGGEST LAST USED 4 agent processes running - 30 transcripts on disk Claude Code claude max - default claude max 20x Codex not signed in GitHub Copilot signed in Gemini signed in 6 open right now - 2.9M tokens between them - oldest untouched 5h * claude Refactor the billing webhook retries payments 225k ██░░░░░░ 13s ago 4 here * claude Port the admin table to components admin-ui 904k ████████ 19m ago 4 here * claude Trace the flaky integration test api 479k ████░░░░ 28m ago 4 here * claude Add a rate limit to the upload route storefront 307k ███░░░░░ 28m ago 4 here * claude Write the migration for the new index scanner 628k █████░░░ 5h ago 4 here * claude Fix the timezone bug in the report analytics 334k ███░░░░░ 5h ago 4 here 24 left on disk, no process behind them claude Split the worker into two queues worker 935k ████████ 7h ago claude Make the search endpoint paginate docs 193k ██░░░░░░ 9h ago claude Update the deploy script for staging infra 196k ██░░░░░░ 27h ago 4 here claude Cache the pricing lookup checkout 182k ██░░░░░░ 2d ago claude Retry the webhook on 5xx payments 518k ████░░░░ 2d ago 4 here claude Move the cron into the scheduler admin-ui 371k ███░░░░░ 2d ago claude Document the auth flow api 287k ██░░░░░░ 2d ago 4 here claude Drop the unused columns storefront 240k ██░░░░░░ 3d ago 4 here claude Tidy the error messages scanner 53k ░░░░░░░░ 6d ago 4 here ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── j/k move h/l pane tab view enter detail O open / search f free port V vibe q quit updated 0s ago
Six tools, each read from the store it already writes: Claude Code, Codex, GitHub Copilot CLI, VS Code chat, Cursor and Gemini CLI. Nothing is installed into any of them, nothing is written back, and no prompt leaves the machine — the reading happens here and stops here.
Does it matter?
Reachability checked by connecting to this machine's real address, not by reading a bind string. Risk as a score with every reason attached, so it can be argued with rather than believed. Use measured over time, because a process can be up for a month and touched this morning.
- It never stops anything. The detail pane prints the command and you run it. A tool that both guesses which process is abandoned and can end it will eventually end the wrong one.
- It opens no listening socket. It connects
outward to see what answers, and binds a candidate port for a moment to check it is free.
Neither calls
listen(). - It sends nothing anywhere. No server, no telemetry, no update check. Agent transcripts are read locally and stay there.
- It says unknown out loud rather than letting a stale process inherit a repository label from whatever owns the port now.
One scan, one model, ten views.
A view is a question asked of the model, never a second trip to the machine. That is why switching views is instant, why the counts always agree with the rows underneath, and why a new view costs a filter rather than a collector.
Install.
One program, no dependencies, nothing system-wide unless your package manager puts it there. Uninstalling leaves one directory you can delete.
# into ~/.local, no root, nothing system-wide $ curl -fsSL https://mr-hunt-007.github.io/portlist/install.sh | sh
Piping into a shell deserves suspicion, so read it first: one tarball, two directories.
$ brew tap Mr-hunt-007/portlist https://github.com/Mr-hunt-007/homebrew-portlist $ brew trust mr-hunt-007/portlist # asked of every third-party tap $ brew install portlist
The trust step is not optional: Homebrew refuses a tap it has not been told to trust, and says so rather than installing anything.
$ pipx install git+https://github.com/Mr-hunt-007/portlist $ pip install git+https://github.com/Mr-hunt-007/portlist
Straight from the repository: the name portlist on PyPI belongs
to an unrelated package, so the distribution will be portlist-tui while the
command stays portlist.
$ git clone https://github.com/Mr-hunt-007/portlist $ cd portlist && python3 portlist.py
No build step. The standard library is the whole dependency list.