A lightweight macOS menubar app that monitors open TCP ports, identifies projects & processes, and lets you kill them — without leaving your workflow.
Ever run lsof -i -P | grep LISTEN to figure out what's hogging port 3000? PortWatch does that for you — continuously, visually, and with one-click kill.
- Zero dependencies — uses native macOS
libprocAPIs, nolsof, no Python, no external tools - Project-aware — groups ports by Docker container, Git repo, or known service
- Role tagging — instantly see which port is your frontend, backend, database, cache, or MCP server
- Non-intrusive — lives in your menubar, no Dock icon
PortWatch lives in your menubar — always visible, never in the way.
- Real-time scanning of all open TCP ports via native macOS APIs (
libproc) - Displays port number, process name, PID, full command line, and uptime
- Auto-refresh every 10s (configurable: 3s – 30s) + manual refresh
Ports are automatically grouped by project using this priority:
| Priority | Method | Example |
|---|---|---|
| 1 | Docker — matches exposed ports with running containers | my-api container on :8080 |
| 2 | Git repo — walks up from process cwd to find .git |
port-watch project |
| 3 | Known ports — PostgreSQL, MySQL, Redis, MongoDB, Elasticsearch | PostgreSQL on :5432 |
| 4 | Other — unidentified processes (shown last) | System services |
Ports grouped by project with role tags, uptime, and one-click actions.
Each process is tagged with a role based on configurable keyword matching against folder name, process name, and command line:
| Role | Default keywords | Icon |
|---|---|---|
| Front | front, web, client, ui, vite, webpack, next, nuxt | 🌐 Globe |
| Back | back, api, server, uvicorn, gunicorn, flask, django, express, fastify | 🖥 Server |
| DB | postgres, mysqld, mysql, mongod, redis-server + db, database (folders) | 💾 Drive |
| Cache | memcached, rabbitmq-server | ⚡ Bolt |
| MCP | mcp-server, mcp_server, fastmcp, modelcontextprotocol | 🤖 MCP |
All keywords are editable in Settings.
- Kill individual process —
SIGTERM→ 4s polling →SIGKILL→ 2s polling → verified dead - Kill entire project — kills all processes in a group in parallel, with per-process verification and detailed report
- Safety confirmation required for "Other" (unidentified) processes
- Open in browser — opens
http://localhost:PORTfor any port
| Indicator | Meaning | Badge |
|---|---|---|
| Zombie process | CLOSE_WAIT sustained across 3 consecutive scans (real socket leak) |
🔴 Red |
| Port conflict | Multiple PIDs on the same port | 🟡 Yellow |
| High CPU | Exceeds threshold (default: 50%) | 🟠 Orange |
| High RAM | Exceeds threshold (default: 500 MB) | 🟠 Orange |
| State | Icon |
|---|---|
| No project ports | Eye closed |
| 1–3 ports | Eye open |
| 4–8 ports | Eye filled |
| 9+ ports or zombie detected in a project | Eye with warning |
Optional macOS notifications (off by default), configurable per category:
- New ports — Off / Projects only / All
- Port conflicts — Off / Projects only / All
Inline settings panel with:
- CPU & RAM alert thresholds (sliders)
- Refresh interval (3–30 seconds)
- Notification preferences
- Detection keywords (editable tags)
- Version info + update checker
- Reset to defaults / Uninstall
Configurable thresholds, notification preferences, and editable detection keywords.
Checks GitHub Releases at launch for new versions. One-click update: downloads, replaces, and relaunches.
- Go to Releases
- Download
PortWatch.zip - Unzip and move
PortWatch.appto/Applications
The app is not signed with an Apple Developer certificate. macOS will block the first launch:
- Double-click
PortWatch.app— macOS shows "cannot be opened" - Open System Settings → Privacy & Security
- Scroll down — you'll see "PortWatch was blocked"
- Click Open Anyway
This is only needed once.
Two options:
| Method | How |
|---|---|
| From the app | Settings → Uninstall PortWatch… (with confirmation) |
| Standalone script | ./uninstall.sh |
Both remove the .app, UserDefaults preferences, caches, logs, and any residual process.
Requires Xcode (free from the App Store) on macOS 26+.
# Debug build
xcodebuild -scheme PortWatch -configuration Debug build
# Release build (.app)
xcodebuild -scheme PortWatch -configuration Release build
# Run tests
xcodebuild -scheme PortWatch test| Component | Technology |
|---|---|
| Language | Swift 6.0 (strict concurrency) |
| UI | SwiftUI MenuBarExtra (.window style) |
| Concurrency | @Observable, @MainActor, Task.detached, TaskGroup |
| Port scanning | Native macOS libproc APIs (import Darwin) |
| Docker detection | docker ps --format json |
| Persistence | UserDefaults |
| Notifications | UNUserNotificationCenter |
| CI/CD | GitHub Actions (macos-26 runner) |
| Min. macOS | 26.0 (Tahoe) |
feature/xxx ──merge──▸ dev ──PR──▸ main ──auto──▸ GitHub Release
│ │
CI tests CI tests + review @Alex375
- Create a branch from
dev:git checkout dev && git checkout -b feature/my-feature - Code, commit, push
- Merge into
dev(CI tests must pass) - Create a PR
dev→main - PR requires CI + review from @Alex375
- On merge to
main: GitHub Actions builds a Release.appand publishes a GitHub Release
Version is read from Info.plist (CFBundleShortVersionString). Bump it before each PR to main — otherwise the release is skipped.
| Change type | Version bump | Example |
|---|---|---|
| Bug fix / tweak | Patch | 1.1.0 → 1.1.1 |
| New feature | Minor | 1.1.0 → 1.2.0 |
| Breaking change | Major | 1.1.0 → 2.0.0 |
Personal use.