You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: rewrite README with actionable user documentation
Apply same standard as weights section to entire file:
- Quick Start: explain install vs start, two-command recommended path
- Scan output: document SIGNALS column and how to interpret it
- Add scan -v use case (tuning / debugging false positives)
- Config: explain grace_period as SIGTERM→wait→SIGKILL sequence
- Config: explain dry_run and how to find results in devreap logs
- Patterns: explain max_duration is scoring signal, not hard kill timer
- Add Troubleshooting section covering 4 common scenarios
- Remove --foreground flag (internal, not user-facing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@@ -21,9 +21,9 @@ When your IDE crashes or you force-quit a terminal, child processes survive as o
21
21
22
22
**84% of developers now use AI tools** (Stack Overflow 2025). Each AI coding session spawns 3-10 background processes. None reliably clean up on crash or force-quit.
23
23
24
-
macOS makes this worse: there's no `PR_SET_PDEATHSIG` (Linux's mechanism to auto-kill children when parents die), no `PR_SET_CHILD_SUBREAPER`, no kernel-level safety net. When your IDE dies on macOS, orphans survive indefinitely.
24
+
macOS makes this worse: there's no `PR_SET_PDEATHSIG` (Linux's mechanism to auto-kill children when parents die), no kernel-level safety net. When your IDE dies on macOS, orphans survive indefinitely.
25
25
26
-
`kill-port` gets **1.16M weekly npm downloads** — that's a million developers manually killing port-squatting processes every week. `fkill-cli` has 6,900 GitHub stars. Both are reactive (you find the problem, you kill it). Nothing proactively detects and cleans up orphans.
26
+
`kill-port` gets **1.16M weekly npm downloads** — that's a million developers manually killing port-squatting processes every week. Both are reactive (you find the problem, you kill it). Nothing proactively detects and cleans up orphans.
27
27
28
28
devreap does.
29
29
@@ -47,25 +47,36 @@ go install github.com/tjp2021/devreap/cmd/devreap@latest
47
47
48
48
## Quick Start
49
49
50
+
**Recommended setup — two commands:**
51
+
50
52
```bash
51
-
#See what's orphaned right now
52
-
devreap scan
53
+
#1. Install as a LaunchAgent so it runs automatically on login
54
+
devreap install
53
55
54
-
# Start the daemon (scans every 30s, kills orphans automatically)
56
+
#2. Start it now (without waiting for next login)
55
57
devreap start
58
+
```
56
59
57
-
# Install as LaunchAgent (auto-start on login)
58
-
devreap install
60
+
That's it. devreap runs in the background, scans every 30 seconds, and kills orphans automatically. You'll get a macOS notification when something gets killed.
59
61
60
-
# Check status
61
-
devreap status
62
+
**Just want to see what's orphaned without running a daemon:**
62
63
63
-
# Run diagnostics
64
-
devreap doctor
64
+
```bash
65
+
devreap scan # show orphan candidates
66
+
devreap scan -v # show all matched processes, including safe ones (useful for debugging)
67
+
```
68
+
69
+
**Check it's running:**
70
+
71
+
```bash
72
+
devreap status
73
+
devreap doctor # full diagnostics — checks config, patterns, process enumeration
65
74
```
66
75
67
76
## What It Looks Like
68
77
78
+
Running `devreap scan` shows matched processes and their scores:
79
+
69
80
```
70
81
$ devreap scan
71
82
@@ -79,11 +90,15 @@ PID NAME PATTERN SCORE AGE STATUS SIGNALS
The **SIGNALS** column shows which signals contributed to the score — what specifically made devreap flag this process. `ppid_is_init` means the parent died. `parent_ide_dead` means no IDE is running. `exceeded_duration` means it's been running longer than this type of process should. If you think something was killed incorrectly, the signals tell you exactly why it was flagged.
94
+
95
+
Use `devreap scan -v` to see all matched processes, including ones below the kill threshold — useful when tuning or debugging false positives.
96
+
97
+
When the daemon kills something, it logs the full reason:
98
+
82
99
```
83
-
$ devreap logs --level info
100
+
$ devreap logs
84
101
85
-
14:02:31 INFO daemon starting
86
-
14:02:31 INFO found 2 orphan candidates
87
102
14:02:31 INFO killed orphan pid=3338 process=node pattern=node-mcp-server score=0.70 signals=[ppid_is_init=0.40,parent_ide_dead=0.30]
88
103
14:02:31 INFO killed orphan pid=7099 process=node pattern=node-mcp-server score=0.65 signals=[ppid_is_init=0.40,exceeded_duration=0.25]
89
104
```
@@ -92,23 +107,21 @@ $ devreap logs --level info
92
107
93
108
### Multi-Signal Orphan Scoring
94
109
95
-
devreap doesn't use binary "is orphan" / "isn't orphan" detection. A process that matches a known pattern gets scored across multiple signals:
110
+
devreap doesn't use binary "is orphan" / "isn't orphan" detection. A process that matches a known pattern gets scored across multiple signals. Each signal has a weight — the score is the sum of weights for signals that fire. If the total reaches the kill threshold (default 0.6), the process is killed.
96
111
97
-
| Signal | Weight | Description |
98
-
|--------|--------|-------------|
99
-
|`ppid_is_init`| 0.40 | PPID is 1 (parent died, reparented to launchd) |
100
-
|`parent_ide_dead`| 0.30 | No IDE process running (VS Code, Cursor, Claude Code, Zed, JetBrains) |
101
-
|`exceeded_duration`| 0.25 | Running longer than the pattern's max duration |
102
-
|`has_listener`| 0.20 | Bound to a listening port (potential orphaned server) |
103
-
|`no_tty`| 0.15 | No controlling terminal attached |
104
-
105
-
**Default threshold: 0.6** — a process needs multiple signals to be flagged. This eliminates false positives.
112
+
| Signal | Weight | When it fires |
113
+
|--------|--------|---------------|
114
+
|`ppid_is_init`| 0.40 | Parent process died — process was reparented to launchd (PPID = 1) |
115
+
|`parent_ide_dead`| 0.30 | No IDE is running anywhere on the machine |
116
+
|`exceeded_duration`| 0.25 | Process has been running longer than its pattern's max duration |
117
+
|`has_listener`| 0.20 | Process is bound to a listening TCP port |
118
+
|`no_tty`| 0.15 | Process has no controlling terminal |
It knows which MCP servers *should* be running. If servers are running but no IDE is active, they're flagged.
139
+
It knows which MCP servers *should* be running. If those servers are running but no IDE is active, they're flagged as orphans. `devreap doctor` will warn you if any of these files exist but can't be parsed.
140
+
141
+
### How Killing Works
142
+
143
+
When a process hits the threshold, devreap sends signals in sequence and waits between each:
127
144
128
-
### Pattern-Aware Signal Strategy
145
+
1.**First signal** (SIGTERM by default, SIGINT for ffmpeg) — asks the process to exit cleanly
146
+
2.**Wait** (grace period, default 5 seconds)
147
+
3.**SIGTERM** (if first signal was SIGINT)
148
+
4.**Wait** (grace period)
149
+
5.**SIGKILL** — force kill if still running
129
150
130
-
Each process type gets the right shutdown signal:
131
-
-**ffmpeg** → `SIGINT` first (writes moov atom for clean MP4, then SIGTERM, then SIGKILL)
132
-
-**Node.js** → `SIGTERM` → `SIGKILL`
133
-
-**Chrome** → `SIGTERM` with extended grace period
151
+
ffmpeg gets SIGINT first because that's the signal that makes it write the MP4 file headers correctly. SIGKILL on ffmpeg produces a corrupted file.
134
152
135
153
### Safety
136
154
137
-
-**PID reuse protection** — verifies process name before sending signals
138
-
-**User isolation** — only kills your processes, never another user's
139
-
-**Blocklist** — postgres, redis, nginx, sshd, and 20+ system processes are protected
-**Config validation** — rejects invalid thresholds, weights, and intervals at startup
155
+
devreap will **never** kill:
156
+
- PID 1 (launchd/init) — hardcoded
157
+
- Its own process — hardcoded
158
+
- Its parent process — hardcoded
159
+
- Any process owned by a different user
160
+
- Anything on the blocklist (postgres, redis, nginx, sshd, and 20+ other system processes by default)
161
+
162
+
Before sending any signal, devreap re-verifies the process name matches what was scanned. If the PID was reused by a different process in between, it aborts.
devreap logs --json # Raw JSON lines — pipe to jq for filtering
181
+
devreap doctor # Diagnostics: config, patterns, permissions, MCP configs
182
+
devreap patterns # List all 18 built-in patterns with durations and signals
183
+
devreap version # Print version, commit, and build date
164
184
```
165
185
166
186
## Configuration
167
187
168
-
Optional. devreap works with zero config using sensible defaults.
169
-
170
-
Create `~/.config/devreap/config.yaml` to customize:
188
+
devreap works out of the box with no config file. Create `~/.config/devreap/config.yaml` only if you need to change something.
171
189
172
190
```yaml
173
-
scan_interval: 30s# How often to scan (min: 1s, max: 24h)
174
-
kill_threshold: 0.6# Score threshold to kill (0.1 - 1.0)
175
-
grace_period: 5s# Time between signals
176
-
dry_run: false # Log what would be killed without killing
191
+
scan_interval: 30s# How often to scan. Min: 1s. Max: 24h.
192
+
kill_threshold: 0.6# Minimum score to kill a process. Range: 0.1 - 1.0.
193
+
# Lower = more aggressive. Higher = more conservative.
194
+
grace_period: 5s# How long to wait between signals (SIGTERM → wait → SIGKILL).
195
+
# Min: 1s. Give processes time to clean up before force-killing.
196
+
dry_run: false # If true, logs what would be killed but doesn't kill anything.
197
+
# Useful for testing — run `devreap logs` to see what it caught.
177
198
178
199
notify:
179
-
enabled: true # macOS notifications on kill
200
+
enabled: true # macOS notifications when the daemon kills something.
180
201
181
-
#Tune signal weights (each 0.0 - 1.0)
182
-
#Higher weight = that signal contributes more to the orphan score.
183
-
#A process is killed when its total score >= kill_threshold (default 0.6).
202
+
#Signal weights — how much each signal contributes to the orphan score.
203
+
#A process is killed when its total score >= kill_threshold.
204
+
#Higher weight = that signal matters more. Each must be 0.0 - 1.0.
184
205
#
185
-
#Common tuning scenarios:
186
-
# Getting false positives on MCP servers while IDE is open?
187
-
# → Lower parent_ide_dead (e.g. 0.1) so IDE presence matters less
188
-
# Running headless servers with no TTY that aren't orphans?
189
-
# → Lower no_tty (e.g. 0.05) so terminal absence matters less
206
+
#When to tune weights:
207
+
# Getting false positives on MCP servers while your IDE is open?
208
+
# → Lower parent_ide_dead (e.g. 0.1)
209
+
# Running intentional background servers with no TTY?
210
+
# → Lower no_tty (e.g. 0.05)
190
211
# Want more aggressive cleanup of long-running processes?
191
212
# → Raise exceeded_duration (e.g. 0.4)
192
-
# Want to rely almost entirely on PPID detection?
193
-
# → Raise ppid_is_init (e.g. 0.7) and lower the others
194
-
weights:
195
-
ppid_is_init: 0.4# Parent process died (PPID reparented to launchd)
196
-
parent_ide_dead: 0.3# No IDE running on this machine
197
-
exceeded_duration: 0.25# Process running longer than pattern's max_duration
198
-
has_listener: 0.2# Process is bound to a listening port
199
-
no_tty: 0.15# No controlling terminal
200
-
201
-
# Note: setting one weight preserves all others at their defaults.
213
+
# Want to rely almost entirely on PPID?
214
+
# → Raise ppid_is_init (e.g. 0.7)
215
+
#
202
216
# You only need to specify the weights you want to change.
203
-
204
-
# Never kill these (in addition to built-in system process protection)
217
+
# Unspecified weights keep their defaults.
218
+
weights:
219
+
ppid_is_init: 0.4# Parent process died (PPID = 1)
220
+
parent_ide_dead: 0.3# No IDE running on this machine
221
+
exceeded_duration: 0.25# Running longer than pattern's max_duration
222
+
has_listener: 0.2# Bound to a TCP listening port
223
+
no_tty: 0.15# No controlling terminal
224
+
225
+
# Processes to never kill, by name. Case-insensitive.
226
+
# These are in addition to the built-in protection list (postgres, redis, nginx, sshd, etc.)
205
227
blocklist:
206
-
- postgres
207
-
- redis-server
208
-
- nginx
228
+
- my-database
229
+
- my-background-worker
209
230
210
-
# Always skip these even if they match a pattern and score above threshold.
211
-
# Use this for persistent servers you intentionally run in the background.
231
+
# Processes to skip even if they score above the threshold.
232
+
# Use this for servers you intentionally run persistently in the background.
233
+
# Matches against process name and command line. Case-insensitive.
212
234
allowlist:
213
235
- my-persistent-mcp-server
214
236
215
-
#Additional pattern files beyond built-ins
237
+
#Paths to additional YAML pattern files to load alongside the built-ins.
216
238
extra_patterns:
217
239
- ~/.config/devreap/my-patterns.yaml
218
240
```
219
241
220
242
## Built-in Patterns
221
243
222
-
18 patterns across 4 categories:
244
+
18 patterns across 4 categories. The **Max Duration** is how long a process of that type is allowed to run before the `exceeded_duration` signal fires — it's not a hard kill timer, it contributes 0.25 to the score.
Run `devreap patterns` for the full list with all fields.
254
+
255
+
## Troubleshooting
256
+
257
+
**Something got killed that shouldn't have been**
258
+
259
+
Run `devreap logs --json | tail -20` to see the last kills with full signal breakdown. The `signals` field shows exactly what triggered it. Then either:
260
+
- Add it to the `allowlist` in your config to permanently protect it
261
+
- Lower the relevant weight if that signal fires too aggressively for your setup
262
+
- Raise `kill_threshold` (e.g. to `0.7`) to require stronger evidence before killing
263
+
264
+
**devreap isn't catching orphans I know exist**
265
+
266
+
Run `devreap scan -v` — this shows all processes matching a pattern, even ones below the threshold. Look at their scores and which signals are firing. If a process has score 0.40 and you want it caught, either lower `kill_threshold` or raise the weight of a signal that's firing.
267
+
268
+
**I want to test what it would kill before letting it run for real**
269
+
270
+
Set `dry_run: true` in your config, then run `devreap start`. It will log everything it *would* kill to `devreap logs` without actually killing anything. Review the logs and adjust config, then set `dry_run: false`.
271
+
272
+
**A process isn't matching any pattern**
273
+
274
+
Run `devreap patterns` to see what's covered. If your process isn't there, you can add a custom pattern — see the CONTRIBUTING guide.
275
+
276
+
**devreap doctor shows a warning**
277
+
278
+
Run `devreap doctor` — it checks config validity, pattern loading, process enumeration, MCP config parsing, and LaunchAgent status. Warnings include an explanation of what to do.
232
279
233
280
## Architecture
234
281
282
+
Single static binary. No runtime dependencies. Cross-compiles to macOS (arm64/amd64) and Linux.
283
+
235
284
```
236
-
cmd/devreap/main.go → entry point
285
+
cmd/devreap/main.go → entry point
237
286
internal/
238
-
cli/ → 10 cobra commands
239
-
scanner/
240
-
process.go → gopsutil process enumeration + port mapping
241
-
scorer.go → multi-signal scoring engine + IDE detection
0 commit comments