proc-janitor
Automatic orphan process cleanup daemon for macOS/Linux
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.
Why?
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.
How It Works
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
Installation
Build from Source
# Copy binary to PATH
One-Line Install (macOS)
&&
This builds the binary, installs it, creates a default config, and sets up a macOS LaunchAgent for auto-start on login.
Cargo Install
Quick Start
# See what's lurking
# Clean orphans immediately
# Start the daemon (runs in background)
# Check status
# Stop the daemon
Configuration
Config file: ~/.config/proc-janitor/config.toml
# How often to scan (seconds)
= 5
# Wait time before killing a new orphan (seconds)
= 30
# Time to wait after SIGTERM before SIGKILL (seconds)
= 5
# Target process patterns (regex)
= [
"node.*claude", # Claude Code
"claude", # Claude CLI
"node.*mcp", # MCP servers
]
# Never kill these (regex)
= [
"node.*server", # Your web servers
"pm2", # Process managers
]
[]
= true
= "~/.proc-janitor/logs"
= 7
Edit with: proc-janitor config edit
Environment Variable Overrides
Every config option can be overridden via environment variables:
PROC_JANITOR_SCAN_INTERVAL=10
PROC_JANITOR_GRACE_PERIOD=60
PROC_JANITOR_SIGTERM_TIMEOUT=15
PROC_JANITOR_TARGETS="python.*test,node.*dev"
PROC_JANITOR_WHITELIST="safe1,safe2"
PROC_JANITOR_LOG_ENABLED=false
PROC_JANITOR_LOG_PATH="/custom/path"
PROC_JANITOR_LOG_RETENTION_DAYS=14
CLI Reference
Core Commands
| Command | Description |
|---|---|
start [--foreground] |
Start the daemon |
stop |
Stop the daemon |
status |
Show daemon status |
scan [--execute] |
Scan for orphans (dry-run by default) |
clean [--dry-run] |
Kill orphaned target processes |
tree [--targets-only] |
Visualize process tree |
dashboard |
Open browser-based dashboard |
logs [-n N] [--follow] |
View logs |
Config Commands
| Command | Description |
|---|---|
config show |
Display current config |
config edit |
Edit config in $EDITOR |
Session Commands
Track related processes as a group:
macOS LaunchAgent
Auto-start on login:
# Install (done automatically by install.sh)
# Uninstall
Configuration Examples
Development Environment
= ["node", "python", "ruby", "webpack", "vite"]
= ["node.*server", "python.*api"]
= 60
Claude Code Only
= ["claude", "node.*claude", "node.*mcp"]
= []
= 30
Safety
- 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 before sending signals
- Dry-run mode — preview cleanup without executing
- Atomic file operations — config and session data use file locking
- Audit logging — every action is logged with timestamps
Architecture
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)
│ ├── config.rs # TOML config + env var overrides
│ ├── logger.rs # Structured logging with rotation
│ ├── session.rs # Session-based process tracking
│ └── visualize.rs # ASCII tree + HTML dashboard
├── resources/
│ └── com.proc-janitor.plist # LaunchAgent template
├── scripts/
│ └── install.sh # One-line installer
├── tests/
│ └── integration_test.rs
├── Cargo.toml
└── LICENSE
~3,000 lines of Rust. 20 tests (13 unit + 7 integration).
Key Dependencies
| Crate | Purpose |
|---|---|
sysinfo |
Cross-platform process table access |
clap |
CLI argument parsing |
nix |
Unix signals (SIGTERM/SIGKILL) |
serde + toml |
Configuration |
tracing |
Structured logging |
daemonize |
Unix daemon support |
fs2 |
File locking |
regex |
Pattern matching |
Performance
- Memory: ~10-20MB resident
- CPU: <1% with default 5s scan interval
- Startup: <100ms
- Scan: <50ms per 1,000 processes
Contributing
Contributions are welcome! Here's how:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -am 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
Development
# Build
# Test
# Run with debug logging
RUST_LOG=debug
# Check for issues
License
Related Projects
| Project | Platform | Scope |
|---|---|---|
| orphan-reaper | Linux | Subreaper-based |
| zps | Linux | Zombie process lister |
| phantom-killer | Windows | PowerShell zombie killer |
proc-janitor fills the gap on macOS where none of these tools work, and provides a configurable, pattern-based approach to orphan cleanup.