[BUG] 2.1.236: silent OAuth token refresh fails behind an https:// CONNECT proxy (ERR_SOCKET_CLOSED) while API traffic succeeds

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 0 comments · opened Sep 3, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

This is a follow-up to #71796, on the version that fixed it. That issue was closed as fixed in 2.1.222 with:

The failure was in the startup connectivity check only (the main API transport already supported https:// proxies), and it was fixed in 2.1.222.

On 2.1.236 the startup check does now pass behind an https:// CONNECT proxy — that fix works. But a third path is still broken: the silent OAuth token refresh (POST https://platform.claude.com/v1/oauth/token) hangs for ~15.4 s and fails with ERR_SOCKET_CLOSED, retries every 15–17 s, and never succeeds.

In the same process, through the same proxy, in the same minutes, normal API traffic to api.anthropic.com works fine. Removing HTTPS_PROXY makes the refresh succeed within seconds.

The proxy accepts and authenticates every tunnel and logs no error at all, so the failure is client-side, after the CONNECT succeeds.

(#71796 was scoped to the startup preflight, so I filed this separately rather than asking to reopen it — happy to have it merged in if you'd rather track it there. Note that a commenter on #71796 also reported "OAuth token exchange during login/authentication" failing with Socket is closed, which may be this same path seen at login time.)

What Should Happen?

The OAuth token refresh should use the same proxy-aware transport as API requests — the change already made for the startup preflight in 2.1.222 — so that a session behind an https:// CONNECT proxy can refresh its token and keep running.

Error Messages/Logs

Claude Code persists telemetry it could not deliver to ~/.claude/telemetry/1p_failed_events.<session>.<id>.json. From one session on 2026-09-02 (additional_metadata is base64-encoded JSON):

| Time (UTC) | Event | Decoded metadata |
|---|---|---|
| 11:55:24.643 | tengu_oauth_token_refresh_failure | error_code: ERR_SOCKET_CLOSED, error_top_frame: cli:105:8707, error_message_hash: 6cbbb8cb8460 |
| 11:55:26.198 | tengu_oauth_token_refresh_starting | — |
| 11:55:41.580 | tengu_oauth_token_refresh_failure | same three values |
| 11:55:44.875 | tengu_oauth_token_refresh_starting | — |

starting → failure is 15.36 s and 15.38 s — a fixed timeout, not jitter. Identical error_code, error_top_frame and error_message_hash on every failure indicate a single code path.

(These files hold only events that failed to send, so this is a lower bound on the number of failures.)

To extract them:

jq -r 'select(.event_data.event_name|test("oauth")) |
       [.event_data.client_timestamp, .event_data.event_name,
        .event_data.additional_metadata] | @tsv' \
  ~/.claude/telemetry/1p_failed_events.*.json
# then: printf '%s' "<metadata>" | base64 -d | jq .

Steps to Reproduce

  1. Run a CONNECT proxy that terminates TLS (any https://-scheme proxy; dumbproxy with -disable-http2 and Basic auth in my case, with a real publicly-trusted cert — no NODE_EXTRA_CA_CERTS needed).
  2. Set HTTPS_PROXY=https://<user>:<pass>@<host>:<port> in ~/.claude/settings.json, leaving platform.claude.com out of NO_PROXY.
  3. Use Claude Code normally until the access token expires (roughly a day). Startup and API traffic work fine throughout — this is past the preflight fixed in 2.1.222.
  4. Observe: the session stalls; ~/.claude/telemetry/1p_failed_events.*.json accumulates tengu_oauth_token_refresh_failure with ERR_SOCKET_CLOSED every 15–17 s; the proxy logs a matching CONNECT per attempt, authenticated, with no error.
  5. Remove HTTPS_PROXY — the refresh succeeds within seconds.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

Not tested on older versions myself. Commenters on #71796 reported 2.1.159 / 2.1.190 working for the (related) proxy paths.

Claude Code Version

2.1.236 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

Environment

| | |
|---|---|
| Claude Code | 2.1.236, Homebrew cask claude-code (stable channel) |
| Runtime | Bun 1.4.0 single-file executable (per strings in the shipped binary) |
| OS | macOS 15.7.3, Darwin 24.6.0, Apple Silicon |
| Terminal | Ghostty (not in the dropdown), zsh |
| Account | Pro subscription (subscription_type: "pro" in telemetry) |
| HTTPS_PROXY | https://<user>:<pass>@<proxy-host>:8443 — TLS listener, Basic auth |
| NO_PROXY | 127.0.0.1,localhost + unrelated internal domains |

The proxy scheme is https://, so the client must do TLS inside TLS: an outer TLS session to the proxy, CONNECT, then an inner TLS session to the origin over that tunnel.

---

Proxy-side log, same window. The proxy logged seven CONNECT //platform.claude.com:443, all authenticated, 15–17 s apart:

11:54:05  11:54:22  11:54:37  11:54:54  11:55:09  11:55:26  11:55:45

Two of them match the telemetry refresh_starting timestamps to within 0.4 s (11:55:26.198 → 11:55:26.611, 11:55:44.875 → 11:55:45.293), confirming one CONNECT per refresh attempt. Across that whole window the proxy emitted 33 INFO lines and zero WARN/ERROR — it did not reject, time out or tear down anything.

---

The only successful refresh was unproxied. The macOS keychain item Claude Code-credentials was rewritten at 2026-09-02 11:56:29Z, with no /login that day, so that write is a successful refresh. It did not go through the proxy:

  • last proxied platform.claude.com CONNECT: 11:55:45, none after
  • all proxy traffic ceased after 11:56:07; nothing at all 11:57–12:00; resumed 12:01
  • HTTPS_PROXY was manually removed from ~/.claude/settings.json at ~11:56:12 and restored ~12:01

The refresh succeeded direct, inside that five-minute gap. Every refresh that actually traversed the proxy failed.

---

The differential. Same process, same proxy, same minutes:

| Host | Client stack | Result |
|---|---|---|
| api.anthropic.com | fetch (main API client) | works |
| platform.claude.com | axios + https-proxy-agent (OAuth) | ERR_SOCKET_CLOSED after 15.4 s |

Per strings in the shipped binary the token exchange uses axios + https-proxy-agent while the main API client uses fetch. That split lines up exactly with what fails and what doesn't — the same shape as #71796, where the preflight used a different client (Mo.get) from the SDK.

---

Ruled out

  • Proxy ALPN / HTTP2. The proxy originally advertised h2; the client negotiated HTTP/2 to the proxy and then sent an HTTP/1.1 CONNECT, which the proxy dropped. Fixed with -disable-http2 before any of the evidence above was collected. The proxy now negotiates http/1.1 when offered h2,http/1.1.
  • Proxy authentication. All CONNECTs in the window log as the authenticated user, not as a 407.
  • TLS interception / IP blocking. The genuine origin certificate chain comes through; the endpoint answers normally.
  • The libraries in isolation. Under Bun 1.4.0, against this same proxy and against locally-hosted http:// and https:// CONNECT proxies with and without Basic auth, all of these succeed: fetch; axios + https-proxy-agent; raw tls.connect({socket}) including TLS-in-TLS. Standalone curl, Node and Bun all reach the token endpoint through the proxy in 0.7–1.8 s and get a normal 400 for a dummy grant.

So neither the proxy nor the libraries reproduce this on their own — only the product does.

---

Hypothesis. "Socket is closed" is the error string Bun's node:tls compatibility layer raises when writing to a tls.connect({ socket }) wrapper whose native handle is gone — i.e. the TLS-over-an-existing-socket path a CONNECT proxy agent uses, and that a direct connection never touches. Something about how the proxy agent is constructed, reused or torn down on the OAuth refresh path appears to leave that socket dead, after which the request hangs to its ~15 s timeout.

---

Questions

  1. Did the 2.1.222 change cover the OAuth token endpoint, or only the two preflight probes?
  2. Is the ~15 s timeout on the refresh request configurable?
  3. Would a --debug log covering a failing refresh help? It is capturable — the failure recurs on its own and timestamps itself in telemetry — but the refresh happens on the product's schedule, so it takes a long-running debug session to catch. Happy to collect one if useful.

Not yet collected: a --debug log spanning an actual failing refresh. Everything above comes from telemetry, the proxy's own logs, the keychain item's modification time, and strings in the shipped binary.

View original on GitHub ↗