Skip to content

[Bug]: Claude CLI stderr is dropped on macOS/Linux, so an early CLI exit shows only "Provider turn failed." and logs no reason #19571

Closed as not planned

Description

@AnobleSCM

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. On macOS or Linux, create an executable script that prints a reason to stderr and exits non-zero:
    #!/bin/sh
    echo "wrapper refused: example reason written to stderr" >&2
    exit 2
  2. Set it as the binary path of a Claude provider instance.
  3. Start a new thread on that instance and send any message.
  4. Look at the thread error, and at the orchestration-v2.claude-query-stream-failed warning in ~/.t3/userdata/logs/server.trace.ndjson.

Standalone check of the spawn difference, using the Agent SDK version bundled in this build (@anthropic-ai/claude-agent-sdk@0.3.276) and the same spawn options as spawnClaudeProcessGroup:

import { spawn } from "node:child_process";
import { query } from "@anthropic-ai/claude-agent-sdk";
const fake = process.argv[2]; // the script from step 1

const groupSpawn = (o) => spawn(o.command, o.args, {
  cwd: o.cwd, env: o.env, signal: o.signal,
  stdio: ["pipe", "pipe", "ignore"], detached: true,
});

async function run(label, extra) {
  async function* prompt() {
    yield { type: "user", message: { role: "user", content: "hi" }, parent_tool_use_id: null, session_id: "" };
  }
  try { for await (const _ of query({ prompt: prompt(), options: { pathToClaudeCodeExecutable: fake, ...extra } })) {} }
  catch (e) { console.log(label, JSON.stringify(e.message)); }
}
await run("SDK spawn:  ", {});
await run("group spawn:", { spawnClaudeCodeProcess: groupSpawn });

Expected behavior

Whatever the CLI (or the configured launch command) printed before exiting can be found. At minimum, a bounded, redacted stderr tail reaches the server log, as it did before this build. Ideally the thread error also says the CLI exited with code 2 before replying, instead of the generic message.

Actual behavior

  • The thread error reads only Provider turn failed.
  • The logged cause is only Claude Code process exited with code 2. The stderr text is kept nowhere.

Code path (main @ bf8ffe8):

  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts L250–255: spawnClaudeProcessGroup spawns with stdio: ["pipe", "pipe", "ignore"]. L770 uses it on every platform except win32. Its comment reads: "Here stderr is dropped instead, so exit errors carry only the code or signal."
  • The Agent SDK fills its stderr tail only in its own spawn, so exit errors from a custom spawnClaudeCodeProcess never include it.
  • L6212–6222: a query that exits mid-turn becomes makeProviderFailure({ cause, class: "transport_error" }) with no message. causeMessage in packages/provider-core/src/server/failure.ts maps only tagged categories, so the UI falls back to Provider turn failed. That fallback predates this change. Before it, the cause logged at L6271 (claude-query-stream-failed) still read ... exited with code 2. stderr: <reason>. Now neither the UI nor the log has the reason.

Regression window: 0.0.46-nightly.20261013.3066 lets the SDK spawn the CLI (stderr piped). 0.0.46-nightly.20261014.3105 adds spawnClaudeProcessGroup (from #19322, which notes this tradeoff).

Suggested fix: in spawnClaudeProcessGroup, pipe stderr and drain it continuously, so the CLI never blocks. Keep a bounded tail, for example the last 4 KiB. Attach it, after redactProviderFailureText, to the cause logged by claude-query-stream-failed. The SDK's spawned-process interface has no stderr, so the adapter has to hold the tail itself. Optionally, when the CLI exits non-zero before its first message, show a fixed message with only the exit code, such as "Claude Code exited with code 2 before it replied. Check the server log for its error output." That keeps provider text out of the UI, in line with causeMessage.

Related: #5943 (asks for the CLI's stderr and exit code when it dies at startup), #16297 (Claude turns fail as transport_error / Provider turn failed. with no cause to go on).

Impact

Minor bug or occasional failure

Version or commit

0.0.46-nightly.20261014.3105 (stderr still logged in 0.0.46-nightly.20261013.3066); still present on main @ bf8ffe8

Environment

macOS 27.0, Apple Silicon. T3 Code Desktop Nightly 0.0.46-nightly.20261014.3105. Provider: Claude, an instance with a custom binary path. Judging by the code, Linux is affected too; Windows is not.

Logs or stack traces

SDK spawn:   "Claude Code process exited with code 2. stderr: wrapper refused: example reason written to stderr"
group spawn: "Claude Code process exited with code 2"

Workaround

Run the configured launch command by hand in a terminal to see its error output.

Activity

  1. juliusmarminge commented on Oct 14, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    This is a known, deliberate tradeoff from #19322, not a regression we'll treat as a bug. That PR spawns the Claude CLI as a process-group leader on macOS/Linux so plugin MCP servers get cleaned up (#3089). The Agent SDK only captures and redacts stderr in its own spawn; piping stderr ourselves without that redaction would put credentials into server logs, so it goes to /dev/null and exit errors carry only the code or signal. The PR description calls this out.

    Workaround for diagnosing an early exit: run the configured binary or wrapper directly in a terminal to see its stderr.

    If you want a redacted stderr tail restored (or a clearer "CLI exited with code N" thread error), please open an Ideas discussion with a proposal for safe redaction. We'd evaluate that there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    via-triageFiled through npx t3 triagewontfixThis will not be worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions