MCP server child processes not cleaned up when Claude Code is killed via signals

Resolved 💬 3 comments Opened Mar 29, 2026 by SPO-Liu Closed Apr 2, 2026

Summary

When Claude Code is terminated via signals (e.g., SIGHUP from tmux kill-session), MCP server child processes (spawned via plugins/channels) are not properly cleaned up. They become orphaned processes (PPID=1) and continue running indefinitely.

Reproduction

  1. Start Claude Code inside a tmux session with a channel plugin (e.g., Telegram):

``bash
tmux new-session -d -s claude-telegram \
'claude --dangerously-skip-permissions --channels plugin:telegram@claude-plugins-official'
``

  1. Verify the MCP server process is running:

``bash
ps aux | grep 'bun.*telegram.*start'
# Also shows child: bun server.ts
``

  1. Kill the tmux session:

``bash
tmux kill-session -t claude-telegram
``

  1. Check for orphaned processes:

``bash
ps -eo pid,ppid,args | grep 'bun.*telegram'
# The bun processes are still alive with PPID=1
``

Impact

Each restart of Claude Code leaves behind orphaned MCP server processes. For channel plugins like Telegram that use long polling, multiple orphaned processes compete for the same bot token's getUpdates slot:

  • Messages are consumed by zombie processes instead of the active session
  • The active session gets 409 Conflict errors and retries in a backoff loop
  • User-visible effect: Telegram messages are silently dropped — the bot appears online but never replies

In my case, 7 orphaned bun processes had accumulated from previous restarts, all polling the same Telegram bot token.

Root cause

Claude Code spawns MCP server processes as independent child processes that are not in the same process group. When the parent process tree is killed:

  1. tmux kill-session sends SIGHUP to the shell
  2. The shell dies → Claude Code receives SIGHUP (or loses its parent)
  3. Claude Code exits, but does not explicitly terminate its MCP server children
  4. MCP server processes become orphans (PPID=1)

The Telegram plugin's server.ts has shutdown handlers for stdin EOF, SIGTERM, and SIGINT — but not SIGHUP. Even so, stdin EOF may not fire reliably when the pipe is broken abruptly rather than closed cleanly.

Suggested fix

One or more of:

  1. Process group: Place MCP server child processes in the same process group as Claude Code, so group-level signals (like SIGHUP from tmux) reach all processes
  2. Explicit cleanup: Register exit/signal handlers in Claude Code that explicitly kill() all spawned MCP server child processes
  3. Plugin-side mitigation: MCP server plugins should also handle SIGHUP (process.on('SIGHUP', shutdown))

Workaround

I added explicit process cleanup to my tmux session management script:

# Kill all telegram MCP server bun processes (runners + their server.ts children)
for pid in $(pgrep -f 'bun.*telegram.*start' 2>/dev/null || true); do
  pkill -P "$pid" 2>/dev/null || true
  kill "$pid" 2>/dev/null || true
done

Environment

  • Claude Code v2.1.87
  • macOS (Darwin 25.2.0)
  • Telegram channel plugin v0.0.4
  • tmux 3.x

View original on GitHub ↗

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