[Regression] Claude Code 2.1.x silently exits with code 1 during HTTP MCP init — loop-protection swallows the underlying exception stacks

Status Fixed / completed
Reported on v2.0.76
Maintainer reply None cached
Activity 4 comments · opened May 6, 2026 · closed May 8, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

Claude Code CLI 2.1.x introduced an "uncaught exception loop" self-protection
mechanism. With ≥3 HTTP transport MCP servers configured, this creates a
diagnostic dead-end:

  1. During MCP server initialization, multiple unhandled promise rejections

are emitted (apparent path: transport _onclose rejecting awaited
Promises with no .catch()).

  1. The self-protection fires once 10 uncaught exceptions occur within 5000ms.
  2. The original stack traces are swallowed by the handler, which logs

only: [ERROR] Uncaught exception loop detected (10 in 5000ms) — forcing shutdown.

  1. The process exits with code 1. SDK consumers (Claude Agent SDK) receive

only Error: Claude Code process exited with code 1 with no actionable
information.

This is an observability regression in the 2.1.x line. The same family
of unhandled MCP rejections in 2.0.76 surfaced as visible stack traces (see
#3184, #2263); the 2.1.x self-protection turns that into a silent exit.

Users have no escape hatch for self-diagnosis: claude ships as a
Bun-compiled binary, so NODE_OPTIONS=--trace-uncaught and Node-level patches
do not apply. The only signal available is the single "loop detected" line.

Steps to Reproduce

  1. Configure mcpServers with three reachable Streamable-HTTP MCP servers.

Each should complete the initialize handshake successfully:

``json
{
"mcpServers": {
"server-a": { "type": "http", "url": "https://example.com/a/mcp",
"headers": { "Authorization": "Bearer ..." } },
"server-b": { "type": "http", "url": "https://example.com/b/mcp",
"headers": { "Authorization": "Bearer ..." } },
"server-c": { "type": "http", "url": "https://example.com/c/mcp",
"headers": { "Authorization": "Bearer ..." } }
}
}
``

  1. Invoke the CLI:

``bash
claude --debug-to-stderr -p "hi" --mcp-config "$(cat config.json)"
``

  1. Observe exit 1 and the single Uncaught exception loop detected line.

No JS stack is printed.

In my own environment, the three HTTP MCP servers used to reproduce this
were web-search-prime and web-reader (both https://open.bigmodel.cn)
and WebSearch (https://dashscope.aliyuncs.com). Bisection suggests the
failure is correlated with the number of concurrently initialized HTTP
transports rather than one specific server, but I have not verified with
HTTP servers outside my configuration.

Downstream SDK impact

The SDK is not required to reproduce the underlying failure, but this is how
the same CLI shutdown surfaces to SDK consumers:

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "hi",
  options: {
    mcpServers: {
      "server-a": { type: "http", url: "...", headers: { Authorization: "Bearer ..." } },
      "server-b": { type: "http", url: "...", headers: { Authorization: "Bearer ..." } },
      "server-c": { type: "http", url: "...", headers: { Authorization: "Bearer ..." } },
    },
  },
})) {
  console.log(message);
}
// → throws: Error: Claude Code process exited with code 1

Expected Behavior

Claude Code should handle both cases gracefully:

  • The transport's connection-close path should attach a .catch() or wrap

awaits in try/catch, so quickly closed transports do not surface as
uncaughtException events.

  • If the loop-detection handler still needs to force shutdown, it should log

the first 1–3 exception stacks before exiting, so users and SDK consumers
can see the underlying McpError: -32000 or other root cause.

Actual Behavior

Bisection matrix (each row is one full CLI startup; "plugin" = any single
--plugin-dir <path>; baseline stdio MCP servers held constant):

| HTTP MCP servers | Plugin | Result |
| ---------------- | :----: | :----: |
| 0 | yes | exit 0 |
| 1 | yes | exit 0 |
| 2 | no | exit 0 |
| 2 | yes | exit 1, loop detected |
| 3 | no | exit 1, loop detected |
| 3 | yes | exit 1, loop detected |

Stdio MCPs alone never trigger the failure regardless of count.

Concrete debug-log fingerprint (HTTP MCPs reach Connection established,
then within ~1s the loop fires):

[DEBUG] MCP server "WebSearch": Successfully connected (transport: http) in 546ms
[DEBUG] MCP server "WebSearch": Connection established with capabilities: {...}
[DEBUG] MCP server "web-search-prime": Successfully connected (transport: http) in 703ms
[DEBUG] MCP server "web-reader": Successfully connected (transport: http) in 928ms
[DEBUG] MCP server "bb-browser": UNKNOWN connection closed after 0s (cleanly)
[ERROR] MCP server "bb-browser" Failed to fetch tools: MCP error -32000: Connection closed
[ERROR] Uncaught exception loop detected (10 in 5000ms) — forcing shutdown
[DEBUG] MCP server "WebSearch": HTTP connection closed after 3s (cleanly)
[DEBUG] MCP server "web-search-prime": HTTP connection closed after 3s (cleanly)
[DEBUG] MCP server "web-reader": HTTP connection closed after 3s (cleanly)
[DEBUG] SessionEnd:other [...] completed with status 0
Note: bb-browser is a stdio MCP server (bb-browser --mcp) that emits -32000 Connection closed immediately after a successful initialize handshake in my setup. It does not trigger the loop by itself, but appears to contribute rejection events when HTTP transports are also present.

Suspected Root Cause

This looks closely related to #3184 and #2263, where MCP transport
close handling surfaced as unhandled promise rejections through stack frames
including _transport.onclose → client.transport.onclose →
l9.onCloseHandler. In the 2.1.x line, the global
process.on('uncaughtException') handler counts events and self-kills at
10/5000ms. Three concurrent HTTP transports each appear to emit ~3–4
unhandled rejections during their tools/list race against early connection
close, comfortably crossing the threshold. The "Uncaught exception loop
detected" string is not present in 2.0.76, which suggests the new shutdown
path / observability regression was introduced in the 2.1.x line.

Proposed Fix

  1. **Attach .catch() (or wrap in try/await) on every transport

_onclose-triggered Promise** in the MCP transport layer. This is the
underlying root-cause fix — same area as #3184 / #2263.

  1. **Log the first 1–3 exception stacks before the loop-detection handler

forces shutdown.** Even without (1), this single change would let users
and SDK consumers diagnose the actual failure instead of seeing only
exit 1.

Related Issues

  • #3184 — MCP error -32000: Connection closed - Node.js uncaught exception

(closed stale, multiple +1s)

  • #2263 — Unhandled Promise Rejection: MCP Connection Closed Error

(closed stale, same family)

  • #55992 — Streamable HTTP MCP: in-flight tool call Promise orphaned forever

on connection drop (open; same transport-layer Promise hygiene weakness,
different lifecycle phase)

  • #56456 — streamable-http MCP sessions never terminated on client shutdown
  • anthropics/claude-agent-sdk-typescript#308 — Silent process exit (code 1)

when calling query() (open; SDK-side perspective on the same opacity)

Environment

  • Platform: darwin (macOS arm64)
  • Claude Code: 2.1.129
  • Claude Agent SDK: 0.2.128 (consumer side; reproducible via the CLI alone

with --mcp-config, so the SDK is not required to trigger)

  • Runtime packaging: Claude Code is a Bun-compiled binary; NODE_OPTIONS=--trace-uncaught does not expose the swallowed stacks

View original on GitHub ↗

4 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/39965
  2. https://github.com/anthropics/claude-code/issues/55992
  3. https://github.com/anthropics/claude-code/issues/46340

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

Xiaoyuan-Liu · 5 months ago

Hi — thanks for the triage. I don't think any of the three is an exact duplicate of #56520.

#56520 appears distinct on three axes:

  1. Phase: it fires during MCP initialization / the first tools/list race, before any user tool call.
  1. Observability regression vs. 2.0.76: the same underlying MCP close-path errors that previously surfaced with visible stacks in #3184 / #2263 are now hidden behind the new loop-detection shutdown path. SDK consumers and direct CLI users alike see only exit 1, with no actionable stack. The CLI also ships as a Bun-compiled binary, so NODE_OPTIONS=--trace-uncaught is not a workaround. The SDK is not required to reproduce this — claude --mcp-config alone is sufficient.
  1. Multi-transport rejection storm: in my bisection, it triggers with ≥3 concurrent HTTP transports, or 2 HTTP transports plus a plugin.

Mapping the suggested issues:

  • #39965 — SDK-side handleControlRequest catch block; fires after the CLI subprocess has exited, inside the consumer's Node process. The stack surfaces normally there. That seems to be a different failure boundary from the loop-handler / Bun observability gap in #56520.
  • #55992 — already cited as related. It likely shares a transport _onclose Promise-hygiene root cause, and a single transport-layer fix may address both. But the user-visible surfaces materially differ: single in-flight orphan → silent hang, versus multi-transport init storm → swallowed-stack process kill. #56520 also carries an independent loop-handler observability defect that should probably be tracked separately even if the transport fix lands jointly.
  • #46340 — UI-state cleanup miss in elicitationHandler.ts: onAbort resolves the Promise correctly with { action: 'cancel' } but forgets to filter the entry out of AppState.elicitation.queue. No unhandled rejection, no transport involvement — different layer, different failure mode.

Happy to fold any of this into the issue body if it helps.

nstaner · 5 months ago

Adding a corroborating data point and a small refinement to the bisection in the original report.

Same failure mode, stdio + HTTP combination on macOS

Hit this on Claude Code in the VS Code extension over the past week (workspaces under ~/Documents/Github/brik/). Identical signature — Uncaught exception loop detected (10 in 5000ms) — forcing shutdown, exit 1, the loop-guard swallows the underlying stacks. Full crash analysis with three-cycle log evidence: brikdesigns/brik-llm#414.

Stdio servers contribute when HTTP transports are present

The original report says "Stdio MCPs alone never trigger the failure regardless of count." Our reproduction confirms the corollary your bb-browser note hinted at: adding a single stdio server on top of a baseline of HTTP/streamable-http servers tips the threshold.

Configuration:

  • 1 local stdio MCP (brik-rag — Python fastmcp, mcp.run(show_banner=False, log_level="WARNING"), connects in ~400ms cleanly)
  • 3 streamable-http MCPs via claude.ai proxy (ClickUp, Notion, Figma) — all complete initialize successfully
  • 5 streamable-http MCPs via claude.ai proxy that fail OAuth (Gmail, Google Drive, Google Calendar, Netlify, Slack, Canva — all in "cached needs-auth" after the first cycle)

Without the stdio server enabled (5+0 stdio): stable across many workspace opens.
With the stdio server enabled (5+1 stdio): crashes every workspace open, every cycle, even after the OAuth-failers are cached and produce zero visible [ERROR] lines.

Empirical evidence the loop-guard counter persists invisibly

Three crash cycles in a single workspace open (VS Code spawns 3+ Claude channels per workspace, each running full extension init):

| Cycle | OAuth-fail [ERROR] count | stdio MCP connected | Result |
| --- | --- | --- | --- |
| 1 | 6 | yes | crash @ ~600ms after first MCP success |
| 2 | 0 (all cached) | yes | crash @ ~135ms after ClickUp success |
| 3 | 0 (all cached) | yes | crash @ ~200ms after ClickUp success |

Cycle 2 and 3 have no visible [ERROR] lines in the 5s window before the crash. The threshold is being tripped by exceptions that aren't being logged at any verbosity level we can see. Consistent with your "stacks swallowed" framing and with the DEBUG-level Failed to stat / Broken symlink / rg error contributors that the brik-llm post-mortem identifies.

Compounder: multi-channel init

workspace open in VS Code spawns 3+ Claude Code channel processes (chat view + sessions sidebar + secondary sidebar, each running its own extension init independently). If a single channel's init contributes N exceptions within 5000ms, all 3 channels are doing the same thing in parallel — anything per-channel gets multiplied. This is structurally invisible to anyone running the bare claude CLI; we only see it because we have logs across ~/Library/Application Support/Code/logs/<timestamp>/window1/exthost/Anthropic.claude-code/Claude VSCode.log.

Our workaround (not a fix)

After spending a workday on this we moved the affected MCP server (brik-rag) off the Claude Code MCP path entirely. Wrapped its Voyage+Supabase operations in a standalone CLI and a Claude skill that calls the CLI via Bash. Sidesteps the extension's MCP plumbing without losing the functionality. Details: brikdesigns/brik-llm#417, PR #419.

We're not the only team that's going to hit this — every workflow that wants a local stdio MCP plus any claude.ai cloud connector is in the firing line.

Endorsing the proposed fix

The two-step fix in the original report is exactly right from where we're sitting:

  1. Attach .catch() (or wrap in try/await) on every transport _onclose-triggered Promise. Same area as #3184 / #2263.
  2. Log the first 1–3 exception stacks before the loop-detection handler force-shuts down.

Even just (2) would have saved us a workday. The "exit 1, no stack" path is genuinely unrecoverable for downstream debugging — Bun-compiled binary means NODE_OPTIONS=--trace-uncaught is dead, and the post-tool log shows successful connections right up to the crash with nothing in between.

Environment

  • Platform: darwin (macOS arm64, M-series)
  • Claude Code: 2.1.138 (VS Code extension Anthropic.claude-code)
  • VS Code: current stable
  • 1 stdio MCP + ~9 claude.ai proxy MCPs as described above
github-actions[bot] · 3 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.