Port Authority: Workspace-Aware Port Inspector for VS Code
A VS Code extension that identifies which process or Docker container is holding a local port, tells you whether it belongs to the project you have open, and stops it through a flow that re-identifies its target before every signal.
Port Authority answers a question developers ask several times a week that no tool answers well: something is already listening on port 3000, what is it? The usual route is switching to a terminal, running lsof, squinting at a PID, and trying to remember whether that was yesterday's dev server or something Docker started. A dozen extensions already kill a port. All of them solve the last five seconds of the problem. This one is built around the part that actually costs time, working out whose process it is and whether stopping it is safe.
The audience is concrete: developers running several local services at once, teams on Docker Compose where a published port says nothing about which container is behind it, and anyone working over Remote SSH, in WSL, or in a devcontainer, where the ports that matter are not on the machine the editor window is running on.
Workspace awareness:
- The extension reads package.json scripts and local .env files and keeps the ports this workspace expects in their own section, so a port that is up but held by a different project reads as FOREIGN rather than green
- Every expectation is traceable back to the exact file and key it came from, so a row explains why it exists rather than just asserting that it does
- The rules are deliberately narrow: -p is only read as a port in scripts that invoke one of twenty-nine known dev servers, a DATABASE_URL pointing at localhost counts while a production host does not, and .env.example and .env.production are skipped entirely
A missed port is a minor annoyance. An invented one puts a permanently red row in someone's sidebar, and that is what makes people switch a feature off.
Container awareness: Ports published by Docker come back as Docker's own plumbing rather than as the thing you started: one shared backend process on Docker Desktop, an anonymous per-port proxy on Linux. Port Authority asks the local daemon which container publishes each port and shows the container name, image, and uptime instead.
- Ownership uses the Compose project working directory, so a container started by a compose file inside an open folder is recognized as yours by the same rule that applies to a process working directory. One started with plain docker run carries no directory and stays unknown rather than being guessed at
- Container rows offer Stop Container, and terminate is never offered on them by any path, because the process behind them is Docker's own and it holds every other published port on the machine
- The daemon is found the way the CLI finds it: DOCKER_HOST, then the active docker context on macOS and Linux, then the per-platform sockets used by Docker Desktop, rootless Docker, Colima, OrbStack, Rancher Desktop, Lima, and Podman. Each candidate must answer a real request first, because a socket left behind by a stopped daemon looks identical to a live one. Only a local socket or named pipe is accepted, and a remote DOCKER_HOST is refused rather than followed
Conflict detection: Terminal and debug console output are watched for bind failures from Node, Go, nginx, Kestrel, Puma, Docker, Vite, and Spring Boot. A match in terminal text is only a hint, so the notification is raised only after a scan confirms a process really is holding that port, which means grepping your own logs for EADDRINUSE produces no popup.
Safety model: Termination is the one destructive path in the extension, so three properties hold regardless of settings.
- The target is re-identified immediately before every signal, not once at the start: a confirmation dialog can sit on screen for an hour, and the PID that was a dev server when it opened can belong to something else by the time it is answered. Identity means PID and start time together, never the PID alone
- A refusal cannot be overridden by configuration. The editor and its parents, PID 1, the Windows system PIDs, and a per-platform list of OS-critical processes are blocked outright, while shared infrastructure such as dockerd or postgres, processes owned by another user, and processes whose name could not be read require a second confirmation
- SIGTERM comes first, always, and force-killing is a separate decision made afterward that re-verifies again before firing. "Port released" appears only after a rescan confirms nothing is listening
Privacy: No telemetry and no network requests: every request is bound to a local socket or named pipe, and the only outbound action anywhere in the extension is Open in Browser, which takes a click. Log output passes through a redaction filter for URL credentials, key=value secrets, and Bearer headers. Restricted Mode is honored, so no workspace file is read until the folder is trusted and a conflict notification drops its terminate button.
Engineering:
- macOS parses lsof field output enriched with ps; Linux reads /proc/net/tcp and /proc/*/fd directly, which spawns no child process and works in minimal devcontainer images that ship neither ss nor lsof; Windows goes through Get-NetTCPConnection and Win32_Process
- One architectural rule drives the layout: the core layer must never import the vscode API. The parsers, inference rules, ownership logic, and kill guard are plain TypeScript modules taking their side effects as injected functions, so they are tested against recorded output from real machines instead of needing an editor. An ESLint rule enforces the boundary
- 165 unit tests across 39 suites on the built-in Node test runner, including deliberate false-positive corpora for both the bind-failure patterns and the inference rules. Integration tests run against a real VS Code instance and scan a socket the test opens itself
- CI runs lint, typecheck, tests, and packaging on Linux, macOS, and Windows, because the scanners are per-platform code paths and a green build on one OS proves nothing about the other two. The Linux job starts a real container and asserts the published port is attributed to it
- Bundled with esbuild, zero runtime dependencies, MIT licensed and open to contributions
The 0.1.0 and 0.2.0 releases were each reviewed adversarially before shipping, and the findings went into the changelog rather than being quietly patched. The container work alone produced six defects, among them a notification that could still offer to terminate Docker's own process and take every container down with it, each fixed with a regression test.
The result is a port tool that behaves like an operations tool rather than a shortcut: it identifies before it acts, it re-checks between the question and the signal, and it reports nothing it has not verified.