Shipping in public

A comment found a gap. Here's the fix.

By Albert 4 min read

Under the open-sourcing announcement, a commenter asked the single best question le has gotten. Paraphrasing: hold a port with a launchd-managed process, kill it, and confirm you're not just chasing ghosts — does it surface the supervisor so you know to disable that first?

They were pointing at the exact failure mode le exists to prevent, one level up. A launchd agent with KeepAlive respawns the moment its PID dies. Kill it and the port is "free" for half a second, then taken again by a new PID. The tool that told you to send TERM just sent you chasing a ghost.

What was true, and what wasn't

We traced their test through the code rather than answer from memory. Two of the three launchd cases were already handled: Homebrew services — launchd jobs under the hood — get routed through the manager (brew services stop <formula>, never the bare PID), and known macOS daemons and app helpers are flagged high-risk and refused outright.

The third case was the gap, and it was exactly the one they described. An arbitrary user agent — your own plist, KeepAlive set, running something generic — classifies by command line as a plain dev process. le would suggest TERM, launchd would respawn it, and the table would show it back within one refresh under a new PID. Visible, at least. But wrong.

The fix

Same day, on main: le now asks launchctl list once per scan and maps PIDs to user-domain labels. A listener whose PID belongs to a launchd job gets launchd in the OWNER column, and its stop becomes the supervisor's own verb:

PORT   PID    WHAT           DIR    RISK    OWNER    STOP WITH
9997   4242   http.server    ~/ops  medium  launchd  launchctl bootout gui/501/com.example.devserver

Three design decisions worth spelling out, because each one is a place we could have gotten it wrong:

Brew keeps precedence. Brew services would also appear in launchctl list — but brew services stop is the correct front-end for those jobs, and it cleans up state a raw bootout wouldn't. The generic launchd route only applies where no better manager claims the process.

Refused rows stay refused — but now name their supervisor. System daemons are launchd-managed by definition, and surfacing their label must not quietly turn a refused row into an auto-stoppable one. So avoid — inspect first stands, and the note gains the missing clue: Managed by launchd as "net.pulsesecure.SetupClient". That's the "so you know to disable that first" part of the question, answered literally.

The bootout gets its own recycle guard. Every le stop already re-verifies the PID's start time so a recycled PID never gets someone else's signal. But a label is a name, not a process — it can be bootout'd and re-bootstrapped onto a different program between scan and stop, the same trap as Docker container names. So immediately before acting, le re-checks that the label still maps to the PID it scanned, and refuses with "rescan" if it doesn't.

Why a comment gets same-day engineering

Because it was a better test plan than anything in our backlog. The commenter didn't report a crash; they described an experiment and asked what would happen. That's the most valuable shape of feedback a tool like this can get — le's whole pitch is that it knows which stop sticks, and they found the case where it didn't.

The fix shipped with the same bar as everything else in the repo: characterization tests pinning the conversion, the brew precedence, and the refused-stays-refused rule; a pure guard function so a future refactor can't silently invert the label check; and a live verification against a real machine — 201 launchd jobs parsed, every matching listener correctly held at "avoid" with its label named. It shipped the same day in v0.1.13brew upgrade alikatgh/tap/le and it's yours.

So, standing invitation: if you can describe an experiment where le gives the wrong verdict, we want it. Open an issue — or just ask the awkward question in a comment thread, apparently. Confirmed gaps get fixed fast, with a regression test in the same commit, and the audit trail stays public either way.

Find the next gap.

MIT-licensed. macOS and Linux. brew install alikatgh/tap/le