Map the system
Read the process tree
Use process identity, parentage, user, start time, and command to understand who launched a program and which context owns it.
10 minute lesson
Every process has an identifier and a parent, except the first system process. On macOS that first process is launchd, always PID 1:
ps -p 1 -o pid,ppid,command
# PID PPID COMMAND
# 1 0 /sbin/launchd
Process ancestry often explains unexpected environment, permissions, and lifecycle. The same program behaves differently depending on who started it, so before you debug a process, find out where it came from.
Inspect a compact process table
ps -axo pid,ppid,user,lstart,command
Read the columns like this: PID is the process, PPID is its parent, USER tells you which account it runs as, lstart gives the exact start time, and COMMAND shows the full executable path. The path matters. Two processes can both be called node, but /opt/homebrew/bin/node and a copy bundled inside an app are different stories.
Find the target PID, then follow PPID values upward:
ps -p 4821 -o pid,ppid,user,command
ps -p 812 -o pid,ppid,user,command # 812 was the PPID of 4821
Repeat until you reach PID 1. A typical chain for a dev server looks like node ← zsh ← login ← Terminal. A background job started by launchd has PPID 1 directly.
Why the ancestry matters
A process started by Terminal, Finder, an application helper, and launchd can receive different environment and restart behavior. Terminal children inherit your shell’s PATH and variables. launchd jobs get a minimal environment and come back automatically if configured to. Privacy permissions are attributed differently too, so a script may read a protected folder from Terminal but fail when launchd runs it.
The classic failure this explains: “it works when I run it by hand, but fails as a background job”. Same binary, different parent, different environment. Compare the two ancestries before blaming the program itself.
Lesson completed