[Proposal] backgroundMaxElapsedSeconds — watchdog for Bash processes left by run_in_background (16 h silent zombie in auto mode)
TL;DR
A Bash tool call with run_in_background: true left a 6-process shell tree alive for 16 h 33 min on my machine (0 % CPU, 0 % RAM, but holding UNIX sockets and file descriptors). The runtime never reaped it because there is no elapsed-time ceiling for background Bash processes. I propose adding backgroundMaxElapsedSeconds as a settings key, with a sane default and a hard cap enforced by the runtime.
This class of failure — the silent stuck process — is not covered by the existing safety story, which focuses on explicit destruction (rm -rf, privilege bypass, network egress). In auto mode it's actually the more dangerous class, because stuck processes:
- don't show up in CPU monitoring (0 %),
- don't trigger error handling (no failure),
- don't time out (nothing is waiting for stdout),
- get forgotten by the model after the next turn.
Incident (INCIDENT-001, 2026-04-21 / 04-22)
The agent emitted this Bash call in auto mode with run_in_background: true:
cd /path/to/dir
npx -y marked --gfm -i 01.md -o /tmp/01.html 2>&1 | tail -3
npx -y marked --gfm -i 02.md -o /tmp/02.html 2>&1 | tail -3
wc -l /tmp/*.html
The snapshot wrapper (source … && shopt … && eval '…') recomposed the command; inside eval the newlines didn't behave as independent statements and the first pipe kept waiting on stdin. Six processes sat in S state for 16 h 33 min:
PID 1236251 /bin/bash -c socat … UNIX-CONNECT:/tmp/claude-http-….sock &
├─ 1236252 socat TCP-LISTEN:3128 → /tmp/claude-http-….sock
├─ 1236253 socat TCP-LISTEN:1080 → /tmp/claude-socks-….sock
├─ 1236254 /proc/self/fd/3 /bin/bash -c source … && eval '…'
└─ 1236255 (re-invoked through pipe)
└─ 1236256 /bin/bash -c source … (blocked on stdin)
All at 0.0 % CPU / 0.0 % MEM. The Claude session that spawned them had long since ended; the processes were adopted by PID 1 (user systemd). The tool had returned command running in background immediately, the model moved on, and nothing ever checked back.
Detection was manual: the user noticed a 16 h shell in their system monitor and asked about it. I only identified it through ps -eo pid,etime,args --sort=-etime with dangerouslyDisableSandbox: true. Exit code after kill -TERM: 143. Tool-use finally marked failed.
Root cause (two independent contributors)
A — model side. I emitted a multi-line command without explicit &&/; separators, assuming bash would treat each line as a separate command. Inside eval '…' it doesn't. Fixable via CLAUDE.md rule and/or a PreToolUse hook (I've added both locally).
B — runtime side. The runtime has no hard upper bound on the lifetime of run_in_background processes. Auto mode biases the model away from re-checking the output file once the tool has returned. This issue is about B. A is the agent's problem to fix; B is an infrastructure gap only the runtime can close safely.
Proposal
// ~/.claude/settings.json
{
"bash": {
"backgroundMaxElapsedSeconds": 1800, // default 30 min
"backgroundMaxElapsedSecondsHardCap": 14400, // absolute ceiling 4 h
"backgroundKillSignal": "TERM",
"backgroundKillGraceSeconds": 10 // SIGKILL after grace
}
}
Semantics
- When the timeout fires, the runtime sends
SIGTERMto the entire process group of the launched subprocess (kill -TERM -pgid). - After
backgroundKillGraceSeconds, escalates toSIGKILL. - A
timeoutevent is written to the process'soutput-fileso the model sees it on the nextTaskOutput/BashOutputcall. backgroundMaxElapsedSecondsHardCapis enforced by the runtime regardless of user config (prevents accidental unlimited timeouts).
Secondary ask (optional). On session start, a cheap reaper pass: any orphan bash whose parent is PID 1 and whose command line contains /tmp/claude-http-*.sock or /tmp/claude-socks-*.sock with etime > hardCap gets reaped. Removes residue from crashed or killed prior sessions.
Why this matters for auto mode specifically
Current safeguards are largely policy/text-driven — regex for explicit destruction, permission prompts, network allowlists. They catch intent-based risks well. Stuck processes have no intent; they accumulate:
| Failure class | Detectable by | Currently covered |
|---|---|---|
| Explicit destructive (rm -rf /) | permission + regex | ✅ |
| Privileged bypass (sudo, --no-verify) | prompt regex | ✅ |
| Unauthorized network egress | sandbox host allowlist | ✅ |
| Silent stuck process | elapsed-time watchdog | ❌ |
| Infinite while loop (no output) | CPU watchdog | ⚠ partial (bash default timeout only) |
| Descriptor leak (many bash open) | fd count | ❌ |
| Accumulation of /tmp/claude-*.sock | inode count | ❌ |
| Exfil via long curl in background | elapsed + connection duration | ⚠ partial |
Auto mode multiplies the three ❌ rows; a single shipped backgroundMaxElapsedSeconds closes the first and reduces the others to non-critical.
What I've deployed locally in the meantime
A 5-level mitigation stack (user-side, independent layers):
- L2 —
zombi-watchdog.sh+ systemd user timer every 15 min. Reaps anybashchild ofsocat /tmp/claude-*.sockwhose parent is not in~/.claude/sessions/*.jsonand whoseetime > 2 h. First real run: 3.9 MB RAM peak, ~1 s CPU. Idempotent, logs to~/.claude/logs/zombi-watchdog.log. - L3 —
PreToolUsehookbash-guard.pythat blocksBashcalls with: (a) multi-line commands lacking&&/;/|separators, (b)run_in_background+npx|node|ollama|curl -N|python -u|wgetwithout atimeout Nprefix, (c)run_in_background+tail -f|watch|inotifywait|less|ping|journalctl -f. - L4 — CLAUDE.md rule: in auto mode, after every
run_in_background, the agent re-checks the output file next turn; every 20 turns runs aps -eo etime,argspass. - L5 — SessionStart hook that reaps orphan Claude-sandbox
bashprocesses before starting.
All five are independent — failure of one doesn't compromise the others. If the proposal ships, L2 becomes a redundant safety net instead of the primary mitigation, which is the correct stance.
Proposed priority
Medium. The incident's economic cost was zero; the doctrinal cost is higher because the class of failure is not covered by the current safety story. A simple backgroundMaxElapsedSeconds default ≤ 30 min would close the gap for 99 % of users without meaningful downside — the cap can be raised explicitly when someone genuinely wants a 4 h background job.
Environment
- Claude Code CLI ·
claude-opus-4-7(1M context tier) - Fedora Silverblue 43 · systemd 258 · bash 5.2.37
- Session id:
fb44fe21-54ee-4d31-ba01-971cea7b0a76(269 turns, auto mode active throughout)
Happy to contribute a PR or refine the spec if useful. Thanks for considering.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗