Skip to main content

portwhisperer

PyPI Python License

A beautiful CLI tool to see what's running on your ports.

Stop guessing which process is on port 3000. portwhisperer gives you a color-coded table of every dev server, database, and background process listening on your machine -- with framework detection, Docker container identification, and interactive process management.

What it looks like

$ ports

╭────────────────────────────╮
│ 🔊 Port Whisperer          │
│ listening to your ports... │
╰────────────────────────────╯

 PORT   ADDRESS  PROCESS  PID    PROJECT          FRAMEWORK   UPTIME  STATUS
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 :3000  local    next     42872  frontend         Next.js     1d 9h   ● healthy
 :5173  LAN      vite     95380  preview-app      Vite        2h 40m  ● healthy
 :5432  LAN      docker   58351  backend-postgr…  PostgreSQL  10d 3h  ● healthy
 :6379  LAN      docker   58351  backend-redis-1  Redis       10d 3h  ● healthy
 :8000  local    uvicorn  61204  api              FastAPI     3h 12m  ● healthy

  5 ports active  ·  Run ports <number> for details  ·  --all to show everything
  • ADDRESS: local means only this machine can connect (127.0.0.1 or ::1). LAN means the server listens on all interfaces, so other machines on your network can reach it. A single network address is shown as is.
  • PROCESS names the tool when an interpreter runs a known one: uvicorn instead of python3.13, vite instead of node.
  • STATUS colors: green = healthy, blue = detached (its parent shell is gone, which is often on purpose), red = zombie.

Every row stays on one line. In a narrow terminal, long names end in ….

Install

uv tool install portwhisperer    # global install (recommended)
uv pip install portwhisperer     # into current virtual env
pip install portwhisperer        # classic pip
pipx install portwhisperer       # isolated global install

Requires Python 3.12+.

Usage

Show dev server ports (default)

ports

Shows dev servers, Docker containers, and databases. System apps (Spotify, Raycast, etc.) are filtered out by default.

Show all listening ports

ports --all

Includes system services, desktop apps, and everything else listening on your machine.

Inspect a specific port

ports 3000
# or
ports --whoisonport 3000

Detailed view: the listen address, the full command line, other ports held by the same process, the repository path and git branch, the container name and image for Docker ports, and the process tree. An interactive prompt then offers to kill the process. The prompt appears only when stdin and stdout are both terminals, so scripts never hang on it.

$ ports 3000

╭────────────────────────────╮
│ 🔊 Port Whisperer          │
│ listening to your ports... │
╰────────────────────────────╯

  Port :3000
  ──────────────────────

  Process          node running next
  PID              42872
  Address          127.0.0.1:3000 (this machine only)
  Status           ● healthy
  Framework        Next.js
  Memory           156.2 MB
  Uptime           1d 9h
  Started          2026-04-02 03:14:00
  Also listens     :9229
  Command          node /Users/dev/frontend/node_modules/.bin/next dev

  Location
  ──────────────────────
  Directory        /Users/dev/frontend
  Project          frontend
  Git Branch       main

  Process Tree
  ──────────────────────
  -zsh (42501)
  └── npm (42860)
      └── node (42872)

  Kill this process: ports kill 3000

  Kill process on :3000? [y/N]

Show all dev processes

ports ps

A beautiful ps aux for developers. Shows all running dev processes (not just port-bound ones) with CPU%, memory, framework detection, and a description column. Docker processes are collapsed into a single summary row.

$ ports ps

╭────────────────────────────╮
│ 🔊 Port Whisperer          │
│ listening to your ports... │
╰────────────────────────────╯

 PID    PROCESS  CPU%  MEM       PROJECT      FRAMEWORK  UPTIME   WHAT
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 592    Docker   1.3   735.5 MB  —            Docker     13d 12h  14 processes
 36664  python3  0.2   17.6 MB   agents       Python     6d 10h   browser_use …
 26408  jest     0.1   9.2 MB    frontend     Node.js    10d 13h  jest --watch
 25752  node     0.0   17.3 MB   api-gateway  Node.js    10d 13h  server.js
 66921  uvicorn  0.0   41.1 MB   api          FastAPI    2h 25m   uvicorn api.…

  5 processes  ·  --all to show everything
ports ps --all    # show all processes, not just dev

Free a port

ports kill 3000           # asks once, then frees the port
ports kill 3000 8080 -y   # several ports, no confirmation
ports kill 3000 --force   # SIGKILL right away

Sends SIGTERM, waits up to 3 seconds for the port to free up, then sends SIGKILL. It reports success only when nothing listens on the port any more, and exits non-zero otherwise.

  • Docker ports run docker stop <container> (docker kill with --force). Docker Desktop and your other containers keep running.
  • Other container engines (OrbStack, Colima, Rancher Desktop, Podman) forward ports through one host process. ports kill never signals that process, because that would drop every port it forwards. It stops the container instead, but only after it confirms that the docker CLI talks to the engine that holds the port. If you run two engines and the active docker context points at the other one, ports kill refuses and stops nothing.
  • Zombies have already exited, so no signal can remove them. ports kill names the parent process that must reap them.
  • Restarting servers: if a watcher such as nodemon starts the server again under a new PID, ports kill says so and fails.

Without a terminal, ports kill needs --yes.

Clean up detached and zombie processes

ports clean

Lists zombie and detached dev server processes, numbered, and asks which to kill (1,3, all, or none, the default). Only targets dev runtimes (node, python, etc.) -- won't touch your desktop apps.

Intentional daemons are left out: processes started with --daemon and jobs managed by launchd, such as brew services.

Watch for port changes

ports watch

Prints the dev servers that are listening now, then one line each time a port opens, closes, or restarts under a new PID. For a Docker port, a new container on the same port counts as a restart.

$ ports watch
  Listening now (2 ports)
  :3000 local ← next (42872) [frontend] Next.js
  :8000 local ← uvicorn (61204) [api] FastAPI

  Watching for changes...
  Press Ctrl+C to stop

  14:02:11 ↻ RESTARTED :8000 local ← uvicorn (61377) [api] FastAPI
  14:02:15 ▲ NEW       :5173 LAN ← vite (95380) [preview-app] Vite
  14:02:40 ▼ CLOSED    :3000 local ← next (42872) [frontend] Next.js
ports watch --interval 5    # poll every 5 seconds instead of 2 (minimum 0.2)
ports watch --all           # include system services and desktop apps
ports watch --json          # one JSON object per line, see Scripting

A restart shows up only when the old and the new process are seen on two polls in a row. If the port stays closed for longer than one interval, you see CLOSED and then NEW.

Open a port in the browser

ports open 3000    # opens http://localhost:3000

Exits 1 and opens nothing when no process listens on the port.

Find a free port

ports next         # first free port from 3000 up
ports next 8000    # first free port from 8000 up

Prints only the number, so you can use it in scripts: npm run dev -- --port $(ports next). It checks the ports that lsof can see, which without sudo means your own processes.

Show the servers of the current project

ports here

Shows only the ports whose server runs inside the current git repository. Outside git, the project is the nearest directory with a project file such as package.json or pyproject.toml. The dev filter does not apply here: every server of the project is shown.

Scripting

Flag Works with What it does
--json ports, ports <port>, ports ps, ports watch Prints JSON instead of a table. No banner, no colour.
-q, --quiet ports, ports <port>, ports ps, ports clean, ports watch, ports here Hides the banner in a terminal.
--version ports Prints the installed version.

The banner prints only when stdout is a terminal. ports | cat prints the table alone.

JSON output. ports --json prints an array of port objects; ports <port> --json prints one object. Each object holds every field the tool knows, such as port, pid, process_name, tool, address, command, cwd, project_name, framework, status, container and image. start_time is ISO 8601. ports ps --json lists Docker processes one by one instead of as one summary row. ports watch --json prints one object per line, with event (snapshot, new, restarted or closed) and time added to the port's fields. After the snapshot lines it prints {"event": "ready", ...}, even when no port is open.

ports --json | jq '.[] | select(.address != "127.0.0.1") | .port'

Exit codes for ports <port>:

Code Meaning
0 A process listens on the port.
1 The port is free. With --json, nothing is printed.
2 The argument is not a valid port (1–65535).
3 The ports could not be checked, for example because lsof failed.
# Start the server only if :3000 is free (exit 1). Exit 3 means the check failed.
ports 3000 --json > /dev/null; [ $? -eq 1 ] && npm run dev

ports kill exits 0 only when every port ends up free. ports open exits 1 when nothing listens on the port. Every command exits 3 when the lsof scan fails, so a failed scan never passes for a free port.

Configuration

ports hides desktop apps and system services by default, using built-in lists. To change what counts as a dev process, create ~/.config/portwhisperer/config.toml (or $XDG_CONFIG_HOME/portwhisperer/config.toml):

# Always show these processes, even without --all
dev_processes = ["air", "hugo"]

# Always hide these processes, unless you pass --all
ignore_processes = ["ollama"]
  • Names match exactly, ignoring case. A name can be the process name (python3.13) or the tool name that the PROCESS column shows (uvicorn).
  • Your entries win over the built-in lists. A name in both of your lists is hidden.
  • The lists apply to ports, ports ps, ports watch and the detached check used by ports clean.
  • If the file has invalid TOML, an unknown key, or a value that is not a list of strings, ports prints one warning and uses the built-in lists.

How it works

Three shell calls, runs in ~0.2s:

  1. lsof -iTCP -sTCP:LISTEN -- finds all listening processes
  2. ps (single batched call) -- retrieves process details for all PIDs at once
  3. lsof -d cwd (single batched call) -- resolves working directories

For Docker ports, a single docker ps call maps host ports to container names and images. It runs only when a container engine (Docker Desktop, OrbStack, Colima, Rancher Desktop or Podman) holds one of the ports.

Framework detection reads package.json dependencies and inspects process command lines.

Platform support

Platform Status
macOS Supported
Linux Planned
Windows Not planned

License

MIT

Metadata

Release files for portwhisperer 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for portwhisperer 1.1.0
File Size Uploaded Provenance Actions
portwhisperer-1.1.0.tar.gz 64.7 kB Without provenance Details for portwhisperer-1.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for portwhisperer 1.1.0
File Interpreter ABI Platform Actions
portwhisperer-1.1.0-py3-none-any.whl Python 3 none any Details for portwhisperer-1.1.0-py3-none-any.whl

Total release size: 101.3 kB

Release files / portwhisperer-1.1.0.tar.gz

Download URL portwhisperer-1.1.0.tar.gz
Size 64.7 kB
Tags Source
SHA-256 checksum
How to use checksums
f4fbb8442b87658361d9ae776478d6887b10554b4555de776d28f1a7b019a683
BLAKE2b-256 checksum
How to use checksums
f8b64b5f0446fe9b0213f439e871f6730de38e0344ae755db1638642f300b927
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / portwhisperer-1.1.0-py3-none-any.whl

Download URL portwhisperer-1.1.0-py3-none-any.whl
Size 36.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d2e800116815ef5e0a929ac8cd492df089c56105a862ceb30bfae7583fc261ae
BLAKE2b-256 checksum
How to use checksums
dda1c6e34dfb385ecf64dd1b77ce738b3f5f6d7e100b177cb9e55c37a4a8987a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for portwhisperer 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page