fix(server): Claude turns say Claude Code exited when it dies at startup#26242
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:
spawnClaudeProcessGroupnow 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.tslists 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-failedserver 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.tsgains 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.prettyincludes 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.tssuites pass: 22 tests. vp run check:changedpassed 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 failure | Provider turn failed. | Claude Code exited with code 1 before it responded. Check the server log for details. |
claude-query-stream-failed cause | Claude Code process exited with code 1 | ClaudeCodeStartupExitError β stderr: error: An internal error occurred (EPERM) β SDK exit error |
| Real Claude CLI 2.1.303, same prompt | OK | OK |
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
Added 3 commits
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.
Thread transfer impact
β Thread transfer remains within every enforced ceiling.
| Provider | Metric | Main baseline | This PR | Impact | PR ceiling | |
|---|---|---|---|---|---|---|
| Codex | Total thread wire | 5.0 KiB | 5.0 KiB | +6 B (+0.1%) | 6.8 KiB | β |
| Codex | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | β |
| Codex | Live turn WebSocket wire | 1.2 KiB | 1.3 KiB | +6 B (+0.5%) | 2.0 KiB | β |
| Codex | Live turn WebSocket decoded | 21.6 KiB | 21.6 KiB | 0 B (0.0%) | 24.4 KiB | β |
| Codex | Live turn messages | 2 | 2 | 0 (0.0%) | 8 | β |
| Claude | Total thread wire | 5.0 KiB | 5.0 KiB | β29 B (β0.6%) | 6.8 KiB | β |
| Claude | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | β |
| Claude | Live turn WebSocket wire | 1.3 KiB | 1.2 KiB | β29 B (β2.3%) | 2.0 KiB | β |
| Claude | Live turn WebSocket decoded | 21.9 KiB | 21.9 KiB | β41 B (β0.2%) | 24.4 KiB | β |
| Claude | Live turn messages | 2 | 1 | β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.
No actionable comments were generated in the recent review.
External image Recent review info
-
External image Run configuration
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: fix/issue-16297
- File: .coderabbit.config.ts
- Commit: 40f3260
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Team
- Run ID:
01323559-3f6a-4184-954f-6c7c7c44208d
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.tsapps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.tspackages/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.tsThe 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.tsFor 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)
mermaid1 sequenceDiagram 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 name Status Explanation Resolution Linked Issues check Warning Direct issue #16297requires a Claude turn launched from T3 to behave like the same authenticated CLI and returnOK. The PR changes only failure reporting.ClaudeCodeStartupExitErrorstill 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 name Status Explanation Title check Passed The title clearly and concisely describes the main change: reporting a named error when Claude Code exits during startup. Description check Passed The 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 check Passed 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 β¦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
#16297requires a Claude turn launched from T3 to behave like the same authenticated CLI and returnOK. The PR changes only failure reporting.ClaudeCodeStartupExitErrorstill 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 returnsOK.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.