claude -p in SDK mode auto-spawns Discord plugin when DISCORD_BOT_TOKEN present (phantom listener)

Resolved 💬 2 comments Opened May 22, 2026 by nazt Closed Jun 21, 2026

Summary

When claude -p "<prompt>" is invoked in an environment that has DISCORD_BOT_TOKEN + DISCORD_STATE_DIR exported (e.g. via direnv), claude's SDK-mode auto-spawns the Discord MCP plugin as a background bun child — even though --channels was NOT passed. That child connects to Discord's gateway and remains alive after claude -p returns. The user gets a phantom listener they never asked for.

Environment

  • @anthropic-ai/claude-code v2.1.146 (the claude binary at ~/.local/bin/claude)
  • Linux (Debian/Ubuntu, white.wg host) — also reproducible on macOS per same-token install patterns
  • claude-plugins-official/discord/0.0.4 plugin installed at ~/.claude/plugins/cache/
  • ~/.claude/settings.json has "enabledPlugins": { "discord@claude-plugins-official": true }
  • direnv sources a .envrc that exports:

``
DISCORD_BOT_TOKEN=...
DISCORD_STATE_DIR=$PWD/.discord
``

Repro

cd /path/to/repo-with-discord-envrc
direnv exec . sh -c 'claude -p "reply only: pong" < /dev/null'

Then, in another shell:

pgrep -af 'bun.*plugins.*discord'
# Expected: nothing (or only your existing claude --channels session)
# Actual:   a new `bun server.ts` from claude-plugins-official/discord/<ver>/
#           with CLAUDE_CODE_ENTRYPOINT=sdk-cli in its env

The phantom child:

  • Holds an ESTAB TCP connection to 162.159.13x.x:443 (Discord gateway)
  • Survives the parent claude -p exit
  • Cannot be cleaned up by the user without kill

Impact

This burned us during a coordinated bot-tenant handoff today (moving volt-oracle from alpha@white.wgnat@white.wg, same Discord token). A pre-flight claude -p to confirm OAuth health on the destination host silently registered a new Discord gateway session, which kicked the alpha-side listener via Discord's "Session Replaced" op9 close — completing the cutover at T+2 instead of the coordinated T+10.

We were lucky the early cutover landed cleanly. In other scenarios this would have caused:

  • Token-conflict between an intentional listener and a phantom one
  • Discord identify-storm if both auto-reconnect
  • "Listening for channel messages" marker appears in a session the user thought was just a one-shot query
  • Hard-to-debug bot-online-but-not-replying states (the phantom serves messages to a claude -p that already exited)

Class of bug: any auth check / health probe / SDK script run in a Discord-configured repo can accidentally become a listener.

Suggested fixes (pick one or combine)

A. Only auto-spawn channel plugins when --channels (or --with-channels <plugin>) is explicitly passed.
The current behavior treats env presence as opt-in; it should require an explicit flag. This matches how claude --channels plugin:discord@... works for interactive sessions.

B. Document the side effect prominently.
At minimum, add to the claude -p / SDK-mode docs: "Channel-capable plugins listed in enabledPlugins will auto-load if their env prerequisites are present, even in -p mode. To suppress, unset DISCORD_BOT_TOKEN / use --no-plugins flag."

C. Emit a stderr warning when SDK-mode invocation triggers a long-lived plugin child:

warn: discord@claude-plugins-official spawned as background MCP child (PID 12345).
      Use --no-channels or unset DISCORD_BOT_TOKEN to suppress.

D. Add --no-plugins / --no-channels flag that explicitly disables MCP plugin auto-load for one-shot invocations. Useful for CI/scripts.

I'd vote A + C. The env-as-opt-in heuristic is too implicit for a process that holds a network socket past the parent's lifetime.

Trace

Reproduced 2026-05-22 ~12:30 +07 on host white.wg, user nat, during a coordinated bot migration. Full incident details in discord-oracle vault: ψ/memory/traces/2026-05-22/ (private repo Soul-Brews-Studio/discord-oracle, can share specific excerpts if helpful).

🤖 ตอบโดย discord จาก Nat → discord-oracle

View original on GitHub ↗

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