MCP server child processes not cleaned up when Claude Code is killed via signals
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
- 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'
- Verify the MCP server process is running:
``bash``
ps aux | grep 'bun.*telegram.*start'
# Also shows child: bun server.ts
- Kill the tmux session:
``bash``
tmux kill-session -t claude-telegram
- 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 Conflicterrors 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:
tmux kill-sessionsendsSIGHUPto the shell- The shell dies → Claude Code receives
SIGHUP(or loses its parent) - Claude Code exits, but does not explicitly terminate its MCP server children
- 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:
- Process group: Place MCP server child processes in the same process group as Claude Code, so group-level signals (like
SIGHUPfrom tmux) reach all processes - Explicit cleanup: Register exit/signal handlers in Claude Code that explicitly
kill()all spawned MCP server child processes - 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
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗