Repository navigation
[Bug]: Claude CLI stderr is dropped on macOS/Linux, so an early CLI exit shows only "Provider turn failed." and logs no reason #19571
Description
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
- 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
- Set it as the binary path of a Claude provider instance.
- Start a new thread on that instance and send any message.
- Look at the thread error, and at the
orchestration-v2.claude-query-stream-failedwarning 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.tsL250–255:spawnClaudeProcessGroupspawns withstdio: ["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
spawnClaudeCodeProcessnever include it. - L6212–6222: a query that exits mid-turn becomes
makeProviderFailure({ cause, class: "transport_error" })with no message.causeMessageinpackages/provider-core/src/server/failure.tsmaps only tagged categories, so the UI falls back toProvider 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
juliusmarminge commentedon Oct 14, 2026
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/nulland 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.