fix(server): Claude turns say Claude Code exited when it dies at startup#26242

open3
main
fix/issue-16297
opened6d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 6d ago

Problem

When the Claude Code CLI exits before it sends its first message, the turn fails about 10 ms after prompt.offer with only Provider turn failed. (transport_error, no code). The cause is not recorded anywhere. One example is macOS denying the CLI access to the project folder, where it prints error: An internal error occurred (EPERM) and exits 1. T3 starts the CLI with stderr set to ignore, so the CLI's own explanation is thrown away. The server log then shows only "Claude Code process exited with code 1".

To reproduce on Linux, point a Claude instance at a CLI that writes that line to stderr and exits 1, then send any prompt. The turn fails with Provider turn failed. and the EPERM line appears nowhere.

Change

  • stderr is kept: spawnClaudeProcessGroup now pipes the CLI's stderr and always drains it. The query runner keeps up to 2,048 characters of it until the CLI's first message, the same limit the SDK uses when it starts the CLI itself.
  • Startup exits get a named error: a nonzero exit or a signal before the first message becomes a ClaudeCodeStartupExitError. failure.ts lists it as a curated error, so the turn reads "Claude Code exited with code 1 before it responded. Check the server log for details."
  • stderr stays in the server log: the stderr tail is attached only to the error's cause chain. The existing orchestration-v2.claude-query-stream-failed server warning prints it. It never reaches the failure message clients see, because stderr can carry paths or tokens.
  • Unchanged cases: a stop (SDK AbortError), exit code 0, a spawn that fails before any exit, and a CLI that dies after it has answered all keep today's error. The Windows spawn path feeds the same tail.

Scope and approval

Fixes pingdotgg/t3code#16297

Triaged via the automated issue flow; GitCafe issue #16297.

Verification

Tests

  • ClaudeAgentSdkQueryRunner.test.ts gains two tests that use the real query runner and a fake CLI. That CLI writes a private-looking EPERM line to stderr and exits 1.
    • names Claude Code's exit code when it exits before answering: the failure message includes "exited" and "code 1" and not the stderr text.
    • keeps the CLI's startup stderr for the server log: Cause.pretty includes the stderr line.
  • Both fail on main (expected 'Provider turn failed.' to include 'exited'; stderr missing from the cause) and pass with the fix.
  • The query-runner and failure.test.ts suites pass: 22 tests.
  • vp run check:changed passed at the merged head: format, lint, knip, typecheck of 23 packages, and 266 tests in 3 files.

Runtime This is a server-only fix with no render path, so there are no captures. I ran a real dev server on a fresh database, t3 thread start --wait with the reporter's prompt, and a stub CLI:

Before (main 36d4d711b7)After (0004ae9b87)
Turn failureProvider turn failed.Claude Code exited with code 1 before it responded. Check the server log for details.
claude-query-stream-failed causeClaude Code process exited with code 1ClaudeCodeStartupExitError β†’ stderr: error: An internal error occurred (EPERM) β†’ SDK exit error
Real Claude CLI 2.1.303, same promptOKOK

Runtime transcript Β· stub CLI

Not checked

  • cannot-verify macos: I did not run an actual macOS folder-access denial. The stub reproduces its observable shape (stderr line, exit 1, no SDK frames).
  • Windows was not executed; its spawn path only adds the same tail.

Models: Claude Opus 5.5 and GPT-6.1-Sol; harnesses: Claude Agent SDK and Codex through T3 Code.

πŸ€– Generated with Claude Code

Checks

23 successful Β· 7 skipped
All checks passed
40f3260

Added 3 commits

  • fix(server): name Claude Code's exit when it dies before responding0004ae9
  • Merge remote-tracking branch 'origin/main' into fix/issue-1629740f3260
  • test(server): reproduce #16297, Claude Code startup death hides its exit and stderr678fea9
Synced from GitHub

Approvability

Verdict: Approved at 40f3260

Macroscope's review found this PR approvable β€” Claude Code startup failures now show an exit code or signal, with a bounded stderr tail available through server logs rather than the user-facing message. The change is limited to failure diagnostics and preserves normal-response and explicit-stop handling.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Thread transfer impact

βœ… Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB+6 B (+0.1%)6.8 KiBβœ…
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiBβœ…
CodexLive turn WebSocket wire1.2 KiB1.3 KiB+6 B (+0.5%)2.0 KiBβœ…
CodexLive turn WebSocket decoded21.6 KiB21.6 KiB0 B (0.0%)24.4 KiBβœ…
CodexLive turn messages220 (0.0%)8βœ…
ClaudeTotal thread wire5.0 KiB5.0 KiBβˆ’29 B (βˆ’0.6%)6.8 KiBβœ…
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiBβœ…
ClaudeLive turn WebSocket wire1.3 KiB1.2 KiBβˆ’29 B (βˆ’2.3%)2.0 KiBβœ…
ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiBβˆ’41 B (βˆ’0.2%)24.4 KiBβœ…
ClaudeLive turn messages21βˆ’1 (βˆ’50.0%)8βœ…

Baseline: 7a059b9 Β· PR result: 40f3260 Β· Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Review in Change Stack β†’

No actionable comments were generated in the recent review.

External image Recent review info
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between 7a059b906e807e5ba81c8f52f3124d1f10d7ae16 and 40f3260adad61bda588d6995ec5ad0e84485e2db.

External image Files selected for processing: 3
  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
  • apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts
  • packages/provider-core/src/server/failure.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Walkthrough

The Claude runner now captures CLI stderr and exit status before the first response. For qualifying early exits, it creates a startup error that includes the exit information and retains stderr in the cause. The provider failure mapper recognizes the startup error message.

Changes: Claude startup error handling

Layer / File(s)Summary
Capture CLI stderr and exit status
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
The process-group spawn pipes stderr to an optional callback. The runner retains up to 2,048 characters of stderr until the first response and records the process exit code and signal.
Convert early exits to provider errors
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts, packages/provider-core/src/server/failure.ts, apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts
For qualifying stream failures, the runner creates a startup error and attaches captured stderr to its cause. The provider failure mapper recognizes the error message. Tests check the reported exit code and retained stderr.

Priority: External image Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

mermaid
1sequenceDiagram
2 participant ClaudeCLI
3 participant ClaudeAgentSdkQueryRunner
4 participant curatedCauseMessages
5 ClaudeCLI->>ClaudeAgentSdkQueryRunner: stderr and exit status before first response
6 ClaudeAgentSdkQueryRunner->>curatedCauseMessages: ClaudeCodeStartupExitError
7 curatedCauseMessages-->>ClaudeAgentSdkQueryRunner: known failure message

Possibly related issues

  • pingdotgg/t3code#26082 β€” Both changes concern early Claude process exits that otherwise produce a generic client error; this change reports exit details and retains stderr.

Suggested reviewers: t3dotgg

Merge Risk: External image Low Β· up to 40f32

Early exits now report an exit code, but server logs may occasionally omit part of the startup diagnostic. This is a bounded diagnostic risk.

Pre-merge checks: Passed 3 Β· Failed 1

Failed Failed checks: 1 warning

Check nameStatusExplanationResolution
Linked Issues checkWarningDirect issue #16297 requires a Claude turn launched from T3 to behave like the same authenticated CLI and return OK. The PR changes only failure reporting. ClaudeCodeStartupExitError still fails a…Fix the startup condition that causes the macOS T3 launch to exit before the first message, so the affected authenticated CLI completes the turn and returns OK. Add automated coverage for the relevant startup path and successful first res…
Passed Passed checks: 3 passed
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main change: reporting a named error when Claude Code exits during startup.
Description checkPassedThe description includes complete Problem, Change, Scope and approval, and Verification sections. It documents behavior, tests, runtime checks, limitations, linked issue, and agent details.
Out of Scope Changes checkPassedThe changes stay within issue #16297. Stderr capture, bounded retention, startup-exit classification, server-log cause details, failure-message curation, and regression tests all address diagnosis or …
Full details: Out of Scope Changes check

Explanation

The changes stay within issue #16297. Stderr capture, bounded retention, startup-exit classification, server-log cause details, failure-message curation, and regression tests all address diagnosis or handling of the reported early Claude turn failure. No unrelated product area or behavior is changed.

Full details: Linked Issues check

Explanation

Direct issue #16297 requires a Claude turn launched from T3 to behave like the same authenticated CLI and return OK. The PR changes only failure reporting. ClaudeCodeStartupExitError still fails a nonzero exit before the first message, and the added tests assert that failure. The PR description states that an actual macOS access-denial run was not verified and does not show that this condition now returns OK.

Resolution

Fix the startup condition that causes the macOS T3 launch to exit before the first message, so the affected authenticated CLI completes the turn and returns OK. Add automated coverage for the relevant startup path and successful first response.

Finishing Touches
πŸ“ Generate docstrings
  • Commit to this branch
  • Create a new PR
πŸ§ͺ Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix Β· Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

[gpt-6.1-sol][Codex] on behalf of shivam.

Handing the diagnostic loss in https://github.com/pingdotgg/t3code/issues/26382 to this PR. Source inspection at main checkout 07f7f976097b56450364fe136aac9253f4f7d8e1 and this PR's head 40f3260adad61bda588d6995ec5ad0e84485e2db finds the same cause: the Linux process-group spawn discards stderr. In this report, the process that fails is the systemd-run scope wrapper, before Claude starts. After an earlier successful scope probe, the cached wrapper remains enabled when the runtime directory later fills. Its error is discarded and the turn becomes Provider turn failed.

This PR pipes that same wrapped process's stderr, retains the bounded startup tail in the logged cause, and gives clients the exit status. It should therefore cover this report's missing diagnostics as well. It does not free the runtime directory or retry without a scope. The client message will still call the failed wrapper β€œClaude Code.”

Unverified: I ran no tests or device repro before handing this over. The full-runtime-directory cause is reporter evidence. A focused additional case would inject an AgentScope wrapper that prints the reported systemd error and exits 1 before executing a sentinel CLI, then assert that the CLI never starts, the client gets the exit status, and the server cause retains the wrapper stderr without exposing it in the client message. No competing fix or PR was created.