Launched this week
ps, top and lsof tell you what is running. witr tells you why. Point it at a process, PID, port, container or file and it traces the chain that explains it - systemd, supervisor, shell or cron - plus who started it, when, from where, and the warnings worth knowing. Run it bare for an interactive TUI with Processes, Ports, Containers and Locks tabs. Or script it: --short for a one-line chain, --json with real exit codes. One static Go binary for Linux, macOS, Windows and BSD.







witr
The thing that'll decide whether I keep it is what it prints when the chain dead ends. A process reparented to init after its parent died, or something started inside a container runtime that hides the real caller, and the honest output there is that it can't tell you. Tools like this get uninstalled the moment they guess once and someone acts on the guess. The --json exit codes are the right instinct, so give the unknown case its own code instead of folding it into success.
witr
@asadmalik901 Thanks for the detailed use case. Two things here, since witr treats them differently. Zombies are handled, a defunct process shows a [zombie] marker and a warning. But a live process that lost its parent isn't correctly handled. It prints shell as source which is inaccurate. I've filed a bug in the repo to handle this correctly. Thanks again.
@pranshuparmar Filing it the same day is more than I expected, thanks. The reason it's worth more than a cosmetic fix is that printing shell is a confident wrong answer, so I stop digging, where unknown would have kept me looking. If the fix ends up being a label change rather than real ancestry recovery, that's still most of the value for me.
witr
@asadmalik901 And I completely agree with you on this Adad. This was a valuable input, thanks again.
@pranshuparmar One thing still open from my first comment: the exit code. If the label changes to unknown but --json still exits 0, any script using witr as a check keeps passing on exactly the case it couldn't answer. A third code, 0 resolved, 1 error, 2 can't determine, is a few lines and it's what makes the honesty machine readable.
The Locks tab caught my eye. Does it cover advisory file locks (flock/fcntl) alongside mandatory ones, or is it scoped to one type? I work with Node.js processes sharing a SQLite file across workers, and the symptoms are usually slow queries rather than visible contention. Knowing whether witr can show which worker holds a read vs write lock on a specific file path would be the thing that saves me reaching for strace.
witr
@hi_i_am_mimo Thanks, good question. I tested this on your exact setup before answering.
Advisory, both kinds. On Linux the Locks tab reads /proc/locks, so fcntl/POSIX byte-range locks and BSD flock locks both appear, and the Type column tells them apart. No separate mandatory category. witr shows locks that are held, not workers blocked waiting.
Read vs write per worker: yes. Two Node workers on one SQLite file, one in a read txn, one in a write txn:
Filing Asad's bug within hours of him raising it says more about where this tool will be in a year than the feature list does.
One push on the product itself. The chain answers what started it. It does not answer why, and you have named the tool after the harder claim.
systemd started it because a unit file says so. The real why is whether anyone still wants it running, and that lives outside the box entirely.
So the output I would value most is the inverse of ancestry. This is running, and nothing on this machine explains why it still should be. Started from a shell session that ended months ago. Unit enabled by someone who has left. Container from a compose file that is no longer in the repo.
That is the thing I actually go hunting for at 2am, and nothing tells me.
Is orphan detection something the ancestry data you already collect could support?
witr
@rabnoor_s Thank you for the kind words and a very fair challenge.
The name came from frustration rather than a claim. The moment it's built for is "why the hell is this port occupied", "why is this even running", and the ancestry chain happened to answer that version of why well enough that the two blended and thus I gave this name.
To answer your other questions, today witr stops at the chain. I made an orphan to check, and it just reports Source: process_api (init) with no warning. But the data to do better is largely already collected: session id and tty sit in the /proc/<pid>/stat slice I parse and currently skip, so checking whether the session leader still exists would give you the dead-shell case directly. systemd may need additional logic, I'll investigate more on this and file issue to enhance the logic.
Thanks again for in-depth technical challenge.
You went and made an orphan to check. That is the part most people skip, and it is why this thread has been worth having.
One subtlety before you build it, because it is what will generate the false positives that get the feature switched off.
A missing session leader does not always mean something went wrong. nohup, disown, setsid and tmux all deliberately detach a process, and those are healthy. The signal you want is not "the session leader is gone", it is "the session leader is gone and nobody meant that".
Deliberate detachment usually leaves a trace. setsid gives a process its own session id equal to its pid, and there is typically no controlling tty from the very start. An accidental orphan looks different: it had a tty, and a session that has since disappeared.
If witr can tell those two apart, the warning is trustworthy and people will leave it on. If it cannot, every tmux user turns it off inside a week.@pranshuparmar
witr
@rabnoor_s Valid points, I'll take care of all these during implementation. Thanks again.
This solves something I've dealt with too many times chaining lsof, ps, and systemctl manually just to trace who actually started a process. The ancestry chain approach (systemd → supervisor → cron → shell) is exactly the missing piece.
for containers, does it trace through to the host-level process, or stop at "docker started this"?
Trying the playground now. Nice work.
witr
@talha_ramzan3 Thank you for your appreciation and trying out playgound. To answer your question, it traces through to the host. Example output pointed at a containerized process from the host:
So you get the real host-side chain through the shim, plus the container name resolved rather than just an ID.
@talha_ramzan3 The manual chain has one property worth not losing on the way to a single answer.
When you run lsof then ps then systemctl yourself, you see the raw evidence at every hop, so you can tell when a step is lying to you. A synthesised "why" is faster and quietly takes that away.
Which is why I would want the evidence behind the chain available on demand, not just the conclusion. Same output, plus what it read to get there.
You save the four commands. You keep the ability to notice when the answer is wrong.
The PID reuse case is the one I'd want to understand. Linux recycles PIDs pretty fast under load, so if I point witr at a PID that already exited and got reused by something unrelated, does it know the two are different processes, or could it walk an ancestry chain that's actually stitched together from two different processes that happened to share a number?
witr
@raffay_sajjad All the actions in TUI are properly guarded, it compares process start time before a kill or renice, so a signal can't land on a recycled PID. The ancestry walk could be improved here, it walks ppid with loop protection but no start-time validation. So the risk does exists for the parent PID. I've filed an issue to handle this appropriately, and thank you for your comment.
@pranshuparmar Good to hear the parent PID case is already filed. that's the one that would've bitten someone in a container teardown scenario, glad it's on the list.
witr
@mrbrjan A daemon reparented to PID 1 still resolves correctly, because the owning unit is read from the process cgroup rather than the parent walk:
The chain there is degraded to systemd → cron, but the real cause is still recovered. Containers work the same way, the cgroup identifies it through the runtime layers.
Where it falls down is when a bare orphan with no supervisor behind it. Then it walks ppid, lands on PID 1, and reports whatever is sitting there as the source, which is inaccurate. The issue to handle this exists.
Thank you for your comment and hope you personally try out witr.