Open Activity Monitor while a few coding agents are running and you get a wall of node and python processes with no idea which is which. Which one is the Claude Code session that has been idle for a day? Which node is a Cursor agent still mid task? Which one is safe to kill?
I kept hitting this, so I built Corral: a free, open-source, native macOS app that shows you every Claude Code, Codex, Cursor, Kiro and Antigravity process on your machine, what project it belongs to, how long it has been sitting there, and how much of your Mac it is holding.
It is Swift and SwiftUI, MIT licensed, needs no special permissions, and makes no network calls. Repo: https://github.com/popyapp/corral
The app itself is simple. The interesting part was everything I learned getting it to work, so that is what this post is about.
The naive approach is to match on process names. That falls apart immediately.
A running cursor-agent never showed up, because nothing in the chain says "cursor" where you would look. The CLI is symlinked as agent, that symlink points at a shell wrapper, and the wrapper execs a bundled node, so the kernel reports the executable as node. The wrapper also exports CURSOR_INVOKED_AS="$(basename $0)", which tells you that being renamed is a supported way to run it, not an accident. Matching on process names is a dead end.
The install directory turned out to be the only stable signal, and it hands you the version for free.
Once you can find the agents, you want to show what each one is doing. That means reading its transcript, and every tool stores state differently.
That last one has a twist worth knowing about. The messages in Cursor's store are plain JSON and perfectly readable, but not one of them carries a timestamp or a sequence number, and the index that orders them is the single blob that is encrypted, with the key sitting in the same database one table over. So "what did it say last" is a question that store will not answer.
I decided not to decrypt it. Reverse engineering an undocumented cipher buys you one release of correctness and a permanent maintenance debt, and Cursor clearly made that blob opaque on purpose. Next to it, though, is a plain meta.json with the session's title, when it last moved, and its working directory in clear text, so the Cursor row honestly says what the session is about rather than pretending to quote it.
Nothing in any of these logs records a pid. So if you answer one row at a time, two agents in the same folder both match the same file, and the idle one gets confidently reported as doing the busy one's work.
There is an exact identifier, CLAUDE_CODE_SESSION_ID names the transcript file, but it is not on the agent process. Claude Code exports it for the things it launches, so you read it off a child. And it still is not enough: it is the session the process was launched as, and a session that has since been resumed writes somewhere else. On one machine an agent's id resolved to no file at all.
What actually fixes it is answering for every agent at once instead of per row, so a log can be claimed once. The agent whose terminal was touched most recently picks first, and an agent left with nothing shows nothing. A wrong line reads as fact, and no line does not.
Reading one environment variable of another process means touching a buffer that is full of API keys. So the reader answers about the one name you write down by hand and compares every other entry as bytes, without ever building a string.
And transcripts get big. One on my machine is 296 MB. The answer is always in the last few entries, so Corral reads session logs backwards from the end and stops once the one-week window closes. That drops the work from 296 MB to 82 MB of reads, about 3 seconds on the first pass and 30 ms per refresh after that.
Once the plumbing works, the app can be honest about state:
On top of that:
caffeinate an agent left running.
// Detect dark theme var iframe = document.getElementById('tweet-2093805908371349861-12'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2093805908371349861&theme=dark" } There is also a menu bar widget, historical CPU and memory graphs, multi-field search, and a disk view that separates cache from superseded versions from real user data.
Corral needs no Full Disk Access, no accessibility, no entitlements. Everything it shows is already readable by any process running as you, through sysctl, proc_pidinfo and proc_pid_rusage. It runs under a hardened runtime with no entitlements, and releases are signed and notarized.
There is exactly one network call in the whole app, an optional "ask Kiro for account usage" that reads the Kiro CLI sign-in token and sends it to Kiro's own servers once per refresh. It is off by default and takes an explicit opt-in.
A full refresh across around 550 processes takes about 8 ms, roughly 0.4% CPU at a two second interval.
If you run more than one coding agent at a time, I think you will like having them all in one honest list. If it gets something wrong on your setup, open an issue, the detection rules are the fun part to improve.