PortScope
Something is on port 3000. PortScope tells you which project.
A macOS app that shows what is listening on your local ports, names the project behind each one, and stops it safely. Not the process name — the project.
“It’s node” is the answer to a different question
You have five node processes. The one holding port 3000 belongs to one of
your checkouts, and nothing on your machine will tell you which.
What lsof and netstat give you
- The port
- The PID holding it
- The process name
- Instantly, with no software to install
What they cannot give you
- Which project. On a machine running several checkouts,
node,pythonandrubyare noise. - A decision. You are handed a PID and left to work out for yourself whether killing it is safe.
From a port number to a project name
Two discovery sources, a resolution step that is allowed to say it does not know, and a stop that is a decision rather than a button.
Two sources, unioned
libproc walks each process’s file-descriptor table, giving a real listening
socket, a real PID and full process metadata. A separate bind() occupancy
sweep catches ports libproc cannot see into.
The project is resolved
Working directory, command line and project markers —
package.json, pyproject.toml, Cargo.toml,
.git and others — resolve to a project name, with a confidence tier of
High, Medium or Low.
Every row says how sure it is
An authoritative row names the process and PID. An occupancy-only row says in use — owner unknown and claims nothing more. Low confidence never carries a project name; it shows the process instead.
Stopping is a decision
Stop sends SIGTERM to that PID only, with a grace period. Force Stop is a separate, deliberate second action. Children are not killed.
Measured, including the number that hurts
Independently measured over 63 real listening-socket processes — the only population PortScope ever displays.
The number to manage is the confidently-wrong rate, not the unrecognised rate. A row that admits it does not know costs you a moment. A wrong name shown confidently is the only outcome that costs trust.
Why that 7.9% is believable
It was measured five times. On four of those five rounds the resolver’s own test bed reported 0% confidently wrong. Independent beds measured 3.7%, 8.8%, 7.2%, 10.9% and 7.9%. The failure was the same every time: testing the fixed case instead of its neighbourhood.
What improved across those rounds was not the headline — it was the
composition, which matters more. By the last round the ordinary subset of 35 scenarios was
0 confidently wrong and 0 wrong at any tier: vite, Next.js standalone, nodemon,
node_modules/.bin shims, --import, --require,
.mjs/.cjs/.ts, and real venv uvicorn, gunicorn and flask.
The remaining 17.9% lives entirely in the 28 deliberately adversarial scenarios. Earlier rounds
missed on plain, common shapes. Those now pass.
Mechanisms, not adjectives
What the app actually does, one line each.
Live table
Port, project or service, process, PID and address, refreshing on a roughly two-second cycle.
Project recognition
A resolved project name with an explicit confidence tier on every row, so you can see how much weight it carries.
Service labels and URL inference
Conservative by design: it offers to open a URL only where the inference is sound.
Detail inspector
Full process, project and socket detail for the row you have selected.
Safe termination
PID-only SIGTERM with a grace period. Force Stop is separate. Protected processes are refused rather than silently failing.
Search and Reveal in Finder
Search across every visible field, and jump straight to the project directory.
What PortScope is not
These are the limits that decide whether you can rely on a row. They are the reason to trust the rest of the page.
- An occupancy row is not a listener.
bind()answers “can I bind this port?”, not “is something listening?” On one real machine, 66 ports reported occupied and only 10 were listeners — the other 56 were local ports held by outbound connections from things like Chrome and Slack. Those rows say “in use — owner unknown”, never “listening”.- A wildcard bind is not a security verdict.
0.0.0.0and::describe binding scope. Whether anything is actually reachable depends on firewall state, network topology and NAT, none of which PortScope can observe. It will never tell you a port is “exposed”.- Not a security scanner, a firewall, or a network monitor.
- It shows local listening sockets on your own machine. That is the whole job.
- Root-owned listeners are visible but not attributable in v1.
sshdand system-account databases appear as occupied ports without an owner. Full attribution needs a privileged helper — planned, not in v1.
Not on the Mac App Store, by measurement
PortScope’s core capability is impossible inside the macOS App Sandbox. That is not an assumption — the same signed binary was run bare and sandboxed, and the difference is the whole product:
| Probe | Unsandboxed | Sandboxed |
|---|---|---|
| TCP listeners discovered | 7 | 0 |
PROC_PIDLISTFDS on other processes | Permitted | EPERM — every process except the app itself |
kill(), including kill(pid, 0) | Permitted | Denied outright |
The file-descriptor table is the only structure linking a socket to a process, so losing it loses the product. Every route around it was tested, and each one failed.
So PortScope will ship direct — Developer ID, hardened runtime, and notarized. Stated as a decision rather than a grievance: it is the distribution the capability requires. The honest upside is no commission, no review latency, and updates that ship when they are ready.
Tell me when it ships
PortScope is in development. There is no download and no purchase yet. Leave an email and we will tell you when that changes.
One email when it ships. Nothing else, and we don’t pass it on.
TraLand.com