I Built ptty Because lsof Wasn't Enough
https://github.com/iCyberon/pttyEvery developer has that one command they type fifty times a day. For me, it’s lsof -i :3000. Or :8080. Or whatever port I vaguely remember assigning to whichever service I was working on twenty minutes ago.
When you’re juggling multiple projects, and I mean genuinely running four or five local services at once, port management becomes a real problem. It’s not just “kill the process on port 3000.” It’s “which of my twelve node processes is the one I actually need to restart, and which ones have been orphaned by a crashed AI orchestrator?”
The Orphan Problem
Let me talk about orphans for a second. If you’re using any of the newer AI coding tools (orchestrators, agents, automated dev environments) you’ve probably noticed they love spawning processes. They’re less enthusiastic about cleaning them up. You end up with zombie node processes eating memory, rogue dev servers still bound to ports, and no easy way to tell which ones are yours and which ones went rogue.
I was spending more time managing processes than writing code. That’s backwards.
Finding the Spark
I saw port-whisperer on X one day and it immediately clicked. Here was someone who had the same frustration and actually built something about it. But it was written in Node.js, and my first instinct was to check if a Go alternative existed. It didn’t.
So I did what any reasonable person would do on a Friday afternoon. I started rewriting it from scratch.
Why Go, Always Go
I won’t pretend to be objective here. I love Go. If something doesn’t absolutely require a different language, I’m reaching for Go. Cross-compilation to every platform with a single command. One binary, no dependencies, no runtime. The code reads like what it does. No over-engineering required.
Go and terminal UIs also go hand in hand thanks to the Bubble Tea ecosystem. I didn’t spend a single second evaluating other TUI frameworks. Sometimes the best decision is just picking something you know works and moving forward.
What ptty Actually Does
At its core, ptty scans your system for listening TCP ports and tells you everything about them: what process owns each port, what framework it’s running, which project it belongs to, and whether it’s healthy or orphaned.
$ ptty list
PORT PROCESS PID PROJECT FRAMEWORK UPTIME STATUS
:8081 node 2642 Firetrail React 5d 15h ● healthy
:1420 node 24051 Mesh React 15h 21m ● healthy
:5037 adb 30507 — — 4d 13h ● orphaned
:6767 node 40623 — Node.js 9d 22h ● healthy
:50884 node 82035 — Node.js 11d 1h ● orphaned
But the real value is in the interactive TUI. Run ptty with no arguments and you get a four-tab dashboard: Ports, Processes, Watch, and Clean. Two key presses and you’re done. That was the design goal. It shouldn’t get in your way, just help you.
Framework Detection
One of the features I’m most proud of (credit to the original port-whisperer for the idea) is the three-tier framework detection:
- Docker image names: if it’s running in a container, we match the image name (PostgreSQL, Redis, nginx, etc.)
- Project files: we walk up the directory tree looking for
package.json,go.mod,Cargo.toml, and read the dependencies to identify Next.js vs plain React vs Vite, etc. - Command keywords: as a fallback, we scan the process command line for telltale strings like
next devorflask run
$ ptty detail 8081
Port :8081
Process node (PID 2642)
Status ● healthy
Framework React
Memory 72.9 MB
CPU 0.1%
Uptime 5d 15h
Directory /Users/vahagn/Work/firetrail/Firetrail
Project Firetrail
Branch main
Process Tree
└─ CMDHub (PID 96856)
└─ npm exec expo run:ios (PID 2093)
└─ node (PID 2642) ← this
Dev Process Filtering
By default, ptty filters out the noise. You don’t need to see Chrome, Spotify, or Finder in your port list. Under the hood, it uses both a blocklist of ~40 consumer apps and an allowlist of ~40 dev-related processes, with keyword matching as a fallback. It’s not perfect, but until I figure out a better approach, it provides the best results. Press a to toggle and see everything if you need to.
Orphan Cleanup
The Clean tab (or ptty clean from the CLI) finds processes that have lost their parent or gone zombie, and lets you kill them in one shot:
$ ptty clean
Found 3 orphaned/zombie processes:
PID 30507 adb — ● orphaned
PID 36922 openclaw-runtime — ● orphaned
PID 82035 node 48.2 MB ● orphaned
Kill all 3 processes? [y/N]
The Hard Parts
Testing
Honestly, the hardest part wasn’t the scanning or the UI. It was testing. When your entire application depends on external data (the operating system, other running processes, listening ports) mocking becomes an exercise in imagination. Every edge case in process name parsing, every quirk of lsof output formatting, every difference between macOS and Linux /proc. All of it needs test coverage.
I’ll admit AI helped a lot with the test writing. But getting the test scenarios right, making sure you’re actually covering the weird real-world cases, that’s still a very human job.
Cross-Platform
The scanner is the only part that varies by platform. macOS uses lsof and ps, Linux reads directly from /proc (no shelling out needed for most operations), and Windows uses netstat and WMI. macOS and Linux were straightforward. Windows… worked. I confirmed it runs, it was slow, and I moved on. The usual suspect.
Terminal UX Is Harder Than GUI
One thing I didn’t expect to learn: good terminal UI/UX is even harder than graphical UI. You have extremely limited space, no mouse (well, barely), and you need to be very precise about what information to show and when. Every character counts. There’s no “just add a tooltip” escape hatch.
The Friday Project
The first working version came together in a single Friday afternoon, hackathon-style. I polished it over the weekend, dogfooded it myself for a few days, then shared v0.9.9 internally with my team at SoftConstruct. Their feedback was great, mostly around dev process detection accuracy and UX tweaks. After a few more iterations, I shipped v1.0.
I included self-update from day one, which might seem premature for a v1. But that’s exactly when you need it most. V1 is always buggy, always evolving fast. Without self-update, your early users get stuck on a broken version and you’re at the mercy of them checking for updates manually. Just press U in the TUI and you’re on the latest version.
The Feedback Trick
There’s one thing I always do when I finish a tool. I show it to the team very casually. “Look what I found.” Without mentioning that I built it. The reactions are genuine. You learn immediately whether something is actually useful or just interesting to you. If someone says “oh cool” and moves on, that’s your answer. If someone says “wait, can it do X?” then you have a roadmap.
What’s Next
I’m thinking about adding port forwarding and background monitoring. But honestly, ptty is meant to stay simple. Two key presses and you’re done. It’s not Netflix, it’s not a browser. It doesn’t need to be more than what it is.
I have ptty sitting in a terminal tab most of the day, and I pipe it through scripts with --json to automate port checks. It’s become one of those tools I forget I built because it just works.
You Have a Problem? Solve It
There has never been a better time to solve your own problems. The tools available to developers today (AI-assisted coding, instant cross-compilation, one-command distribution) mean that the distance between “this annoys me” and “I shipped a fix” has never been shorter.
Software development has changed dramatically, but the core value hasn’t. It’s still about solving problems. Big or small. If lsof -i :3000 is the most-typed command in your terminal history, maybe it’s time to build something better.
ptty on GitHub. MIT licensed, works on macOS, Linux, and Windows. Install with a single command:
curl -sSL https://raw.githubusercontent.com/iCyberon/ptty/main/install.sh | sh