Summary
On a self-hosted remote session, when the claude provider returns 401 (expired/unrefreshed OAuth token), the current CLI (0.2.8-dev.x) silently swallows the user's message — the UI accepts it, it's dropped, the turn finalizes as completed, and no error surfaces. An older version (0.2.1) surfaced it: Failed to authenticate. API Error: 401 The socket connection was closed unexpectedly. For more information, pass verbose: true in the second argument to fetch().
Two concurrent sessions on the same machine (sharing ~/.claude/.credentials.json): the 0.2.1 session showed the 401; the new-version session went silent. claude /login (re-mint token) fixed both.
Root cause (traced)
The message is delivered to the agent (deliveredMaterializedMessage:true); it dies inside the claude Agent-SDK turn. The 401 arrives as an in-band system/api_error event (SDK retries internally, "attempt 1/11") rather than a thrown error. Two independent gates then fail open, so nothing surfaces:
- Key mismatch in the classifier.
claudeRemoteAgentSdk.ts → classifyClaudeConnectedServiceRuntimeAuthFailure (isClaudeRuntimeAuthFailureEvidence) only inspects 401 under keys api_error_status / status / statusCode / status_code. The SDK's api_error system event carries the code under key error_status (plus a short error string like "Connection error."). Neither matches → classifier returns null → onRuntimeAuthFailureEvent never fires.
- Connected-service gate. Even if it fired,
surfaceClaudeConnectedServiceRuntimeAuthFailure returns early when there is no connected-service selection. Installs that auth via shared ~/.claude/.credentials.json OAuth have connectedServiceSelection: null, so this surface path is also dead.
Net: the turn finalizes completed, no sendSessionEvent error is emitted, the message is lost.
Why 0.2.1 surfaced it
0.2.1's SDK path threw on the 401 (isClaudeAgentSdkAuthenticationError matched the thrown message: API Error: 401, Failed to authenticate, OAuth token has expired); the throw propagated to the launcher catch → exit-code-1 branch → sendSessionEvent({type:'message', ...}) via the UI error formatter → surfaced. The move to in-band structured classification (+ internal retry + connected-service gate) is what introduced the silent swallow.
Environment
Self-hosted, remote sessions, claude auth via ~/.claude/.credentials.json OAuth (no connected-service injection). New-version session affected; 0.2.1 session on the same host/credentials surfaced correctly.
Repro
- Remote session on the current CLI, claude authed via credentials.json OAuth (no connected-service selection).
- Make claude return 401 (expire the OAuth token with a failed refresh, or invalidate it).
- Send a message.
- Observe: message accepted by UI, dropped, turn ends
completed, no error shown.
Expected
Provider auth failures (401) surface to the user (as 0.2.1 did) so they know to re-auth — including on credentials.json-OAuth installs with no connected-service selection.
Suggested fixes
- Add
error_status to the key set in isClaudeRuntimeAuthFailureEvidence (the SDK api_error event uses it).
- Surface provider auth failures regardless of connected-service selection —
surfaceClaudeConnectedServiceRuntimeAuthFailure should not early-return for credentials.json-OAuth sessions; route to surfacePrimarySessionRuntimeIssue so it's not a silent no-op.
Evidence (logs, anonymized)
- Client 401:
[ERROR] API error (attempt 1/11): 401 401 The socket connection was closed unexpectedly. For more information, pass verbose: true ... (and an earlier {"type":"authentication_error","message":"Invalid authentication credentials"}).
- Runtime:
[claudeRemoteAgentSdk] Claude runtime auth diagnostic ... connectedServiceSelection:null; inbound system event with keys attempt/error/error_status/max_retries/retry_delay_ms; [pendingQueue] ... deliveredMaterializedMessage:true; [pendingQueue] turn-end drain trigger ... terminalStatus:"completed" → task_complete; no error event emitted. Two consecutive turns dropped this way before a manual claude /login recovered it.
Summary
On a self-hosted remote session, when the claude provider returns 401 (expired/unrefreshed OAuth token), the current CLI (
0.2.8-dev.x) silently swallows the user's message — the UI accepts it, it's dropped, the turn finalizes ascompleted, and no error surfaces. An older version (0.2.1) surfaced it:Failed to authenticate. API Error: 401 The socket connection was closed unexpectedly. For more information, pass verbose: true in the second argument to fetch().Two concurrent sessions on the same machine (sharing
~/.claude/.credentials.json): the0.2.1session showed the 401; the new-version session went silent.claude /login(re-mint token) fixed both.Root cause (traced)
The message is delivered to the agent (
deliveredMaterializedMessage:true); it dies inside the claude Agent-SDK turn. The 401 arrives as an in-bandsystem/api_errorevent (SDK retries internally, "attempt 1/11") rather than a thrown error. Two independent gates then fail open, so nothing surfaces:claudeRemoteAgentSdk.ts→classifyClaudeConnectedServiceRuntimeAuthFailure(isClaudeRuntimeAuthFailureEvidence) only inspects 401 under keysapi_error_status/status/statusCode/status_code. The SDK's api_error system event carries the code under keyerror_status(plus a shorterrorstring like "Connection error."). Neither matches → classifier returnsnull→onRuntimeAuthFailureEventnever fires.surfaceClaudeConnectedServiceRuntimeAuthFailurereturns early when there is no connected-service selection. Installs that auth via shared~/.claude/.credentials.jsonOAuth haveconnectedServiceSelection: null, so this surface path is also dead.Net: the turn finalizes
completed, nosendSessionEventerror is emitted, the message is lost.Why 0.2.1 surfaced it
0.2.1's SDK path threw on the 401 (isClaudeAgentSdkAuthenticationErrormatched the thrown message:API Error: 401,Failed to authenticate,OAuth token has expired); the throw propagated to the launchercatch→ exit-code-1 branch →sendSessionEvent({type:'message', ...})via the UI error formatter → surfaced. The move to in-band structured classification (+ internal retry + connected-service gate) is what introduced the silent swallow.Environment
Self-hosted, remote sessions, claude auth via
~/.claude/.credentials.jsonOAuth (no connected-service injection). New-version session affected;0.2.1session on the same host/credentials surfaced correctly.Repro
completed, no error shown.Expected
Provider auth failures (401) surface to the user (as
0.2.1did) so they know to re-auth — including on credentials.json-OAuth installs with no connected-service selection.Suggested fixes
error_statusto the key set inisClaudeRuntimeAuthFailureEvidence(the SDK api_error event uses it).surfaceClaudeConnectedServiceRuntimeAuthFailureshould not early-return for credentials.json-OAuth sessions; route tosurfacePrimarySessionRuntimeIssueso it's not a silent no-op.Evidence (logs, anonymized)
[ERROR] API error (attempt 1/11): 401 401 The socket connection was closed unexpectedly. For more information, pass verbose: true ...(and an earlier{"type":"authentication_error","message":"Invalid authentication credentials"}).[claudeRemoteAgentSdk] Claude runtime auth diagnostic ... connectedServiceSelection:null; inboundsystemevent with keysattempt/error/error_status/max_retries/retry_delay_ms;[pendingQueue] ... deliveredMaterializedMessage:true;[pendingQueue] turn-end drain trigger ... terminalStatus:"completed"→task_complete; no error event emitted. Two consecutive turns dropped this way before a manualclaude /loginrecovered it.