I killed a process. It came back. I killed it again. It came back again. Turns out the process was managed by launchd – macOS’s init system – and killing it was like pulling a weed without getting the root. The system just restarted it.
This sent me down the rabbit hole of “how do processes actually work on this machine.” Here’s everything I wish I’d had on one page.
Finding processes
Before you can kill something, you have to find it.
ps aux – the classic
ps aux | grep openclaw
This lists every running process on the system. The columns that matter:
USER PID %CPU %MEM VSZ RSS TTY STAT STARTED TIME COMMAND
shiyuan 12345 0.3 1.2 420000 48000 ?? S 10:30AM 0:02 /usr/local/bin/openclaw gateway
- PID – the process ID. You’ll need this for
kill. - COMMAND – what’s actually running. Sometimes the full path reveals where a binary lives.
- STAT –
S= sleeping,R= running,Z= zombie (dead but its parent hasn’t acknowledged it).
Tip: ps aux | grep something always matches itself (the grep process). You’ll see a line like grep --color=auto openclaw. Ignore it, or use pgrep instead.
pgrep – just give me the PID
pgrep openclaw # PIDs only
pgrep -fl openclaw # PIDs + full command line
Cleaner than ps aux | grep. No self-match noise. The -f flag matches against the full command line, not just the process name – useful when the binary is named something generic but the arguments are distinctive.
top and htop – live view
top # built-in, every system has it
htop # much better UI, install via brew/apt
top is the Activity Monitor of the terminal. It shows CPU/memory usage in real time, sorted by resource consumption. Press q to quit, P to sort by CPU, M to sort by memory.
htop is top but actually usable – color-coded, mouse-friendly, tree view of parent/child processes, and you can kill processes directly from the UI with F9. If you don’t have it:
brew install htop # macOS
sudo apt install htop # Ubuntu/Debian
Killing processes
kill – send a signal
kill 12345 # sends SIGTERM (signal 15) -- "please exit gracefully"
kill -9 12345 # sends SIGKILL (signal 9) -- "die now, no cleanup"
kill doesn’t mean “kill.” It means “send a signal.” kill 12345 sends SIGTERM, which asks the process to shut down. The process can catch SIGTERM, clean up resources, save state, and exit. Most well-behaved programs do this.
kill -9 sends SIGKILL, which the process cannot catch or ignore. The kernel terminates it immediately. Use this when a process is stuck and won’t respond to SIGTERM. But know the tradeoff: no cleanup means potential data corruption, leaked temp files, orphaned lock files.
My rule: Try kill first. Wait a couple seconds. If it’s still there, then kill -9.
killall – by name
killall openclaw # kill all processes named "openclaw"
Matches on the process name (not the full command line). Convenient when you don’t want to look up PIDs. Be careful – if you run killall python, you’ll kill every Python process on the machine.
pkill – pattern matching
pkill openclaw # kill processes matching "openclaw"
pkill -f "openclaw gateway" # match against full command line
pkill is the kill counterpart to pgrep. The -f flag matches against the full command line, same as pgrep -f. More flexible than killall since it does substring matching.
The process that won’t stay dead – launchd
This is where I got stuck. I ran kill, confirmed the process was gone, and five seconds later it was back. That’s launchd.
What is launchd?
launchd is macOS’s process supervisor – it’s PID 1, the first process that starts when your Mac boots, and the parent of everything else. It manages background services (daemons) and per-user agents. If a managed process dies, launchd restarts it. That’s its job.
This is why kill doesn’t work on managed processes. You’re fighting the supervisor. It’s like trying to close a door while someone on the other side keeps opening it.
launchctl list – what’s launchd managing?
launchctl list | grep openclaw
Output looks like:
12345 0 com.openclaw.gateway
The columns: PID, last exit status, label. The label is the service identifier – you’ll use it for every other launchctl command.
Stopping a managed service
There are multiple ways, and they do different things:
# Stop the service (it may restart based on plist config)
launchctl stop com.openclaw.gateway
# Unload the service definition (prevents restart, deprecated but still works)
launchctl unload ~/Library/LaunchAgents/com.openclaw.gateway.plist
# Modern equivalent of unload -- removes it from the boot session
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.openclaw.gateway.plist
launchctl stop sends a stop signal but leaves the service registered. If the plist has KeepAlive set to true, launchd will restart it.
launchctl bootout (or the older launchctl unload) removes the service from the current session. It won’t restart until the next login or until you explicitly reload it. This is what you want when you’re trying to actually stop something.
Where plist files live
Services are defined by .plist files in specific directories:
| Directory | Scope | Runs as |
|---|---|---|
~/Library/LaunchAgents/ |
Current user only | Your user |
/Library/LaunchAgents/ |
All users, on login | Each user |
/Library/LaunchDaemons/ |
System-wide, on boot | root (usually) |
/System/Library/Launch* |
Apple’s own services | Don’t touch these |
If something keeps respawning and you can’t find it in ~/Library/LaunchAgents/, check /Library/LaunchDaemons/. It might be a system-level daemon.
Reading a plist
cat ~/Library/LaunchAgents/com.openclaw.gateway.plist
The important keys:
<dict>
<key>Label</key>
<string>com.openclaw.gateway</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/openclaw</string>
<string>gateway</string>
<string>start</string>
</array>
<key>KeepAlive</key>
<true/> <!-- THIS is why it keeps coming back -->
<key>RunAtLoad</key>
<true/> <!-- starts automatically on login -->
</dict>
KeepAlive: true is the culprit. It tells launchd “if this process dies for any reason, restart it.” The only way to stop it is to remove it from launchd entirely with bootout, or edit the plist.
The full “make it stop” sequence
# 1. Find the service
launchctl list | grep openclaw
# 2. Remove it from the current session
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.openclaw.gateway.plist
# 3. Verify it's gone
pgrep openclaw # should return nothing
launchctl list | grep openclaw # should return nothing
# 4. (Optional) Prevent it from starting on next login
# Either delete the plist or set RunAtLoad to false
Linux systemd – the same idea, different commands
Linux’s equivalent of launchd is systemd. Same concept: a process supervisor that manages services, restarts them on failure, and starts them on boot. Different commands.
# Check service status
systemctl status openclaw
# Stop a service
systemctl stop openclaw
# Prevent it from starting on boot
systemctl disable openclaw
# Stop AND prevent boot start
systemctl disable --now openclaw
# See logs
journalctl -u openclaw -f # -f for live tail
The key difference: systemd has a clean separation between “running right now” (stop/start) and “starts on boot” (enable/disable). launchd mixes these in the plist file.
| I want to… | macOS (launchctl) | Linux (systemctl) |
|---|---|---|
| See if it’s running | launchctl list | grep name |
systemctl status name |
| Stop it now | launchctl bootout gui/$(id -u) /path/to.plist |
systemctl stop name |
| Prevent boot start | Remove or edit the plist | systemctl disable name |
| View logs | log show --predicate 'process == "name"' |
journalctl -u name |
| Start it | launchctl bootstrap gui/$(id -u) /path/to.plist |
systemctl start name |
Finding what’s using a port
“Something is already listening on port 8080.” This happens all the time.
# macOS
lsof -i :8080
# Linux
lsof -i :8080
# or
ss -tlnp | grep 8080
lsof stands for “list open files” – and in Unix, network sockets are files. -i :8080 means “show me everything using port 8080.”
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 14523 shiyuan 22u IPv4 0x1234 0t0 TCP *:8080 (LISTEN)
Now you know it’s a node process with PID 14523. Kill it, or realize you forgot to stop your dev server from yesterday.
Common lsof patterns
lsof -i :8080 # what's on port 8080?
lsof -i -P -n # all network connections (fast, no DNS lookup)
lsof -p 12345 # all files opened by PID 12345
lsof +D /var/log # what's reading/writing in this directory?
Background processes – jobs, nohup, disown
Sometimes you want to start something and forget about it.
The basics
# Run in background (attached to terminal)
./long-running-script.sh &
# See background jobs for this shell
jobs
# Bring job #1 back to foreground
fg %1
# Send foreground job to background (Ctrl+Z first, then:)
bg %1
The & at the end runs a command in the background, but it’s still tied to your terminal session. Close the terminal and the process dies.
nohup – survive terminal close
nohup ./long-running-script.sh &
nohup means “no hangup.” When you close a terminal, it sends SIGHUP (hangup signal) to all its child processes. nohup makes the process ignore that signal. Output goes to nohup.out by default.
# With explicit output redirect
nohup ./script.sh > output.log 2>&1 &
disown – retroactive nohup
You forgot to use nohup and the process is already running:
./long-running-script.sh & # oops, forgot nohup
disown %1 # detach it from the shell
disown removes the job from the shell’s job table. The shell won’t send SIGHUP when it exits. The process lives on.
The hierarchy:
&– background, still attached to terminalnohup– survives terminal close, but you planned aheaddisown– survives terminal close, retroactive
Quick reference
| I want to… | Command |
|---|---|
| Find a process by name | pgrep -fl name |
| See all processes | ps aux or htop |
| Kill gracefully | kill PID |
| Kill forcefully | kill -9 PID |
| Kill by name | pkill name |
| Find what’s on a port | lsof -i :PORT |
| Stop a macOS daemon | launchctl bootout gui/$(id -u) /path/to.plist |
| Stop a Linux service | systemctl stop name |
| Run and forget | nohup ./script.sh & |
| Detach running job | disown %1 |
Most of the time you just need pgrep, kill, and lsof. But when a process won’t die, knowing about launchd/systemd is the difference between a five-second fix and twenty minutes of confused googling.