app-server-broker.mjs processes leak when Claude Code session ends abnormally

Resolved 💬 3 comments Opened Apr 20, 2026 by nhod Closed May 27, 2026

Summary

app-server-broker.mjs processes (spawned by the Codex plugin) accumulate indefinitely when a Claude Code session terminates abnormally. They are never cleaned up, consuming significant RAM and swap.

Root cause

The broker is intentionally daemonized (PPID=1) so it survives across reconnects. Its only exit paths are:

  1. A broker/shutdown RPC message sent by Claude Code
  2. SIGTERM or SIGINT
  3. Fatal startup error

There is no idle timeout and no parent-death detection. If Claude Code exits via crash, OOM kill, SIGKILL, or terminal close before sending broker/shutdown, the broker runs forever — listening on a stale Unix socket that no client will ever reconnect to.

Evidence

On a single developer machine running CoachClaw (a Claude Code-heavy project), 38 orphaned brokers accumulated over ~2.5 days:

$ ps -eo pid,ppid,etime,cmd | grep app-server-broker | grep -v grep | wc -l
38
# All had PPID=1 (orphaned), oldest was 2d 15h

Combined resource usage of the 99 broker+child processes:

  • ~1.35 GB RSS
  • ~3.8 GB swap consumed (on a 4 GB swap device — completely exhausted)

After pkill -f app-server-broker.mjs:

  • Free RAM: 281 MiB → 4.0 GiB
  • Swap used: 4.0 GiB → 207 MiB

Repro

  1. Start any Claude Code session that triggers a Codex tool call (spawns a broker)
  2. Kill the terminal, trigger an OOM, or otherwise hard-exit Claude Code without graceful shutdown
  3. Observe that the app-server-broker.mjs process remains running indefinitely with PPID=1

Suggested fix

Add an idle-timeout to the broker: if zero clients have been connected for N minutes (e.g. 15–30), call shutdown() and exit. This is a safe safety net — a live Claude Code session will reconnect well within that window, so legitimate brokers are unaffected.

// Example sketch — after server.listen():
let lastActivityAt = Date.now();
// reset on every socket connect/disconnect
const idleCheckInterval = setInterval(() => {
  if (sockets.size === 0 && Date.now() - lastActivityAt > IDLE_TIMEOUT_MS) {
    clearInterval(idleCheckInterval);
    shutdown(server).then(() => process.exit(0));
  }
}, 60_000);

Workaround

A cron job running every 15 minutes that kills brokers older than 30 minutes:

ps -eo pid,etimes,cmd --no-headers \
  | awk '/app-server-broker\.mjs/ && $2 > 1800 {print $1}' \
  | xargs -r kill

Environment

  • Claude Code CLI (latest)
  • Linux 6.8.0-110-generic
  • Codex plugin v1.0.2

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗