Automatic orphan process cleanup daemon for macOS (Linux experimental)
proc-janitor detects and terminates orphaned processes that linger after their parent terminal or application exits. No more zombie Node.js instances eating up your RAM.
When you close a terminal (Ghostty, iTerm2, VS Code, etc.), child processes like Claude Code, Node.js, or MCP servers often keep running as orphans (PPID=1). Each one silently consumes 200-300MB of memory.
This happens because:
- Terminals don't always send SIGHUP when closed via Cmd+W or the window button
- macOS lacks
prctl(PR_SET_PDEATHSIG)— there's no native way to auto-kill children when the parent dies - Processes escape process groups via
setsid,disown, or background execution
You end up manually running pkill -f claude every few hours. proc-janitor automates this.
Every 5 seconds (configurable):
1. Scan process table for PPID=1 processes
2. Match against target patterns (regex)
3. Skip whitelisted processes
4. Wait grace period (default 30s) to avoid false positives
5. Send SIGTERM → wait → SIGKILL if unresponsive
6. Log everything
curl -fsSL https://raw.githubusercontent.com/jhlee0409/proc-janitor/main/scripts/install-binary.sh | shDownloads the latest pre-built binary for your platform (macOS/Linux, x86_64/ARM64) and installs it to /usr/local/bin.
brew install jhlee0409/tap/proc-janitor
# Start as a background service
brew services start proc-janitorcargo install proc-janitorPre-built binaries for every release are available on the Releases page. Download the archive for your platform, extract, and place proc-janitor in your PATH.
| Platform | Architecture | Download |
|---|---|---|
| macOS | Apple Silicon (M1/M2/M3/M4) | proc-janitor-v*-aarch64-apple-darwin.tar.gz |
| macOS | Intel | proc-janitor-v*-x86_64-apple-darwin.tar.gz |
| Linux | x86_64 | proc-janitor-v*-x86_64-unknown-linux-gnu.tar.gz |
| Linux | ARM64 | proc-janitor-v*-aarch64-unknown-linux-gnu.tar.gz |
git clone https://github.com/jhlee0409/proc-janitor.git
cd proc-janitor
cargo build --release
sudo cp target/release/proc-janitor /usr/local/bin/git clone https://github.com/jhlee0409/proc-janitor.git
cd proc-janitor && bash scripts/install.shThis builds the binary, installs it, creates a default config, and sets up a macOS LaunchAgent for auto-start on login.
cargo install proc-janitor
sudo cp resources/proc-janitor.service /etc/systemd/user/
systemctl --user enable --now proc-janitor# If installed via install.sh
bash scripts/install.sh --uninstall
# If installed via Homebrew
brew uninstall proc-janitor# Create a config file with explanations
proc-janitor config init
# Detect orphaned processes (safe, no killing)
proc-janitor scan
# Watch mode: continuously scan every 5 seconds
proc-janitor scan --watch 5
# Kill all detected orphans
proc-janitor clean
# Kill only specific PIDs
proc-janitor clean --pid 12345 67890
# Kill only orphans matching a pattern
proc-janitor clean --pattern "node.*mcp"
# Kill only orphans older than 5 minutes
proc-janitor clean --min-age 300
# Interactive mode: confirm each kill
proc-janitor clean --interactive
# Start the daemon (runs in background)
proc-janitor start
# Check status
proc-janitor status
# Stop the daemon
proc-janitor stop
# Restart the daemon (stop + start)
proc-janitor restart
# Reload config without restart (send SIGHUP)
proc-janitor reload
# View cleanup statistics (last 7 days)
proc-janitor stats
proc-janitor stats --days 30
# View process tree filtered by pattern
proc-janitor tree --pattern "node"
# Diagnose issues
proc-janitor doctor
# Generate shell completions (add to your .zshrc/.bashrc)
proc-janitor completions zsh > ~/.zfunc/_proc-janitor
# Get JSON output
proc-janitor -j status
proc-janitor -j config show
proc-janitor -j scanConfig file: ~/.config/proc-janitor/config.toml (all platforms)
# How often to scan (seconds, 1–3600)
scan_interval = 5
# Wait time before killing a new orphan (seconds)
grace_period = 30
# Time to wait after SIGTERM before SIGKILL (seconds)
sigterm_timeout = 5
# Target process patterns (regex)
targets = [
"node.*claude", # Claude Code
"claude", # Claude CLI
"node.*mcp", # MCP servers
]
# Never kill these (regex)
whitelist = [
"node.*server", # Your web servers
"pm2", # Process managers
]
[logging]
enabled = true
path = "/Users/you/.proc-janitor/logs" # absolute path required (~ not expanded)
retention_days = 7Edit with: proc-janitor config edit
Every config option can be overridden via environment variables. Values outside the valid range are rejected with a warning and the default is kept.
| Variable | Valid Range | Example |
|---|---|---|
PROC_JANITOR_SCAN_INTERVAL |
1–3600 | 10 |
PROC_JANITOR_GRACE_PERIOD |
0–3600 | 60 |
PROC_JANITOR_SIGTERM_TIMEOUT |
1–60 | 15 |
PROC_JANITOR_TARGETS |
comma-separated regexes | "python.*test,node.*dev" |
PROC_JANITOR_WHITELIST |
comma-separated regexes | "safe1,safe2" |
PROC_JANITOR_LOG_ENABLED |
true / false |
false |
PROC_JANITOR_LOG_PATH |
path under $HOME |
"/Users/you/.proc-janitor/logs" |
PROC_JANITOR_LOG_RETENTION_DAYS |
0–365 | 14 |
PROC_JANITOR_LOG_PATH is validated for safety: directory traversal (..), system paths (/etc/, /usr/, etc.), and paths outside $HOME are rejected. /var/log/ is allowed as a standard log location.
| Option | Short | Description |
|---|---|---|
--json |
-j |
Output results in JSON format (supported by: status, config show, scan, clean, stats) |
--quiet |
-q |
Suppress non-essential output (hints, spinners). Useful for scripts and cron jobs. |
| Command | Description |
|---|---|
start [-f|--foreground] [-d|--dry-run] |
Start the daemon. With --dry-run, scan and log without killing. |
stop |
Stop the daemon |
status |
Show daemon status (systemctl-style with uptime) |
scan [-w|--watch SECS] |
Detect orphaned processes (safe, no killing). With --watch, continuously scan at interval. |
clean [--pid PIDs] [--pattern REGEX] [-i|--interactive] [--min-age SECS] |
Kill orphaned target processes (all by default, or filter by PID/pattern/age). With -i, confirm each kill. |
restart [-f|--foreground] [-d|--dry-run] |
Restart the daemon (stop + start) |
reload |
Reload daemon configuration (sends SIGHUP, no restart needed) |
stats [--days N] |
Show cleanup statistics from the last N days (default: 7). Supports --json. |
tree [-t|--targets-only] [-m|--pattern REGEX] |
Visualize process tree (optionally filter by regex pattern) |
logs [-f|--follow] [-n N] |
View logs (N: 1–10000, default 50) |
version |
Show version and build information |
doctor |
Diagnose common issues and check system health |
completions <shell> |
Generate shell completions (bash, zsh, fish, powershell) |
| Command | Description |
|---|---|
config init [--force] [--preset NAME] [-y|--yes] |
Create config (auto-detects orphans, or use preset: claude, dev, minimal). Use --yes to skip prompts. --list-presets to see available presets. |
config show |
Display current config |
config edit |
Edit config in $EDITOR (validates after save, supports flags like code --wait) |
config env |
Show all environment variable overrides with current values |
config validate |
Validate configuration file and show a summary |
Track related processes as a group. Each tracked PID stores its start_time for PID reuse detection — session cleanup verifies process identity before sending signals, even hours after registration.
proc-janitor session register --name "my-session" --source terminal
proc-janitor session register --id custom-id --name "dev" --source vscode --parent-pid 1234
proc-janitor session track <session-id> <pid>
proc-janitor session list
proc-janitor session clean <session-id> [--dry-run]
proc-janitor session unregister <session-id>
proc-janitor session auto-clean [--dry-run]Supported --source values: claude-code, terminal, vscode, tmux, or any custom string.
The daemon watches config.toml for changes and automatically reloads when the file is modified. No restart needed — just edit and save.
When the daemon kills orphaned processes, it sends a macOS notification via Notification Center showing the count and process names.
Every cleanup action is recorded to ~/.proc-janitor/stats.jsonl as append-only JSON Lines. Each entry includes a timestamp, the number of processes cleaned, and details of each kill (PID, name, signal used, success/failure). The file is automatically rotated (to stats.jsonl.old) when it exceeds 5 MB.
Auto-start on login:
# Install (done automatically by install.sh)
launchctl load ~/Library/LaunchAgents/com.proc-janitor.plist
# Uninstall
launchctl unload ~/Library/LaunchAgents/com.proc-janitor.plist- Whitelist protection — matching processes are never killed
- System PID guard — PIDs 0, 1, 2 are always protected
- Grace period — orphans get time to self-cleanup before termination
- PID reuse mitigation — verifies process identity (start_time) before sending signals, including session-tracked PIDs
- Daemon identity verification —
stopconfirms the PID file points to an actual proc-janitor process before sending signals - Symlink protection — refuses to write to symlinks at predictable paths (
~/.proc-janitor/), preventing local symlink attacks - TOCTOU-safe session store — exclusive file lock held across full read-modify-write cycle
- Guided setup — shows a helpful hint when no target patterns are configured, guiding users to
config init - Scan before clean —
scanis always safe (detection only),cleanis always destructive (with optional filters) - Atomic file operations — config and session data use file locking with fsync for crash safety
- Directory permissions —
~/.proc-janitor/created with0o700(owner-only access) - Audit logging — every action is logged with timestamps
proc-janitor/
├── src/
│ ├── main.rs # Entry point
│ ├── cli.rs # CLI argument parsing (clap)
│ ├── daemon.rs # Daemon lifecycle (start/stop/status)
│ ├── scanner.rs # Orphan process detection
│ ├── cleaner.rs # Process termination (SIGTERM/SIGKILL)
│ ├── kill.rs # Shared kill logic (system PID guard, PID reuse check, polling)
│ ├── doctor.rs # Health checks and diagnostics (8 checks)
│ ├── config.rs # TOML config + env var overrides + presets
│ ├── config_template.toml # Commented config template (embedded at compile time)
│ ├── logger.rs # Structured logging with rotation
│ ├── session.rs # Session-based process tracking (TrackedPid with start_time)
│ ├── util.rs # Shared utilities (color detection, symlink protection)
│ └── visualize.rs # ASCII process tree
├── resources/
│ └── com.proc-janitor.plist # LaunchAgent template
├── scripts/
│ └── install.sh # One-line installer
├── tests/
│ └── integration_test.rs
├── Cargo.toml
└── LICENSE