From e9984ebc074576df27131463d578d3a864b12e64 Mon Sep 17 00:00:00 2001 From: jordan Date: Thu, 29 Jan 2026 23:12:01 -0700 Subject: [PATCH] fix: Include stderr and troubleshooting help in Claude Code errors When Claude fails to execute, error messages now include: - Captured stderr output from the failed command - Troubleshooting commands to exec into pod and run `claude login` Co-Authored-By: Claude Opus 4.5 --- changelog/v0.10.12.md | 15 ++++++ deployments/k8s/base/rdev-api.yaml | 2 +- .../adapter/codeagent/claudecode/adapter.go | 51 ++++++++++++++++--- 3 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 changelog/v0.10.12.md diff --git a/changelog/v0.10.12.md b/changelog/v0.10.12.md new file mode 100644 index 0000000..22476e2 --- /dev/null +++ b/changelog/v0.10.12.md @@ -0,0 +1,15 @@ +# v0.10.12 + +**Released:** 2026-01-29 + +## Changes + +fix: Include stderr and troubleshooting help in Claude Code error messages + +When Claude fails to execute, the error now includes: +- The actual stderr output from the failed command +- Troubleshooting commands to exec into the pod and run `claude login` + +--- + +**Image:** `ghcr.io/orchard9/rdev-api:v0.10.12` diff --git a/deployments/k8s/base/rdev-api.yaml b/deployments/k8s/base/rdev-api.yaml index b5dde3a..563f0e0 100644 --- a/deployments/k8s/base/rdev-api.yaml +++ b/deployments/k8s/base/rdev-api.yaml @@ -24,7 +24,7 @@ spec: serviceAccountName: rdev-api containers: - name: rdev-api - image: ghcr.io/orchard9/rdev-api:v0.10.11 + image: ghcr.io/orchard9/rdev-api:v0.10.12 imagePullPolicy: Always ports: diff --git a/internal/adapter/codeagent/claudecode/adapter.go b/internal/adapter/codeagent/claudecode/adapter.go index b5454ed..18e6b63 100644 --- a/internal/adapter/codeagent/claudecode/adapter.go +++ b/internal/adapter/codeagent/claudecode/adapter.go @@ -131,6 +131,7 @@ func (a *Adapter) Execute(ctx context.Context, req *domain.AgentRequest, handler // Stream and parse output var wg sync.WaitGroup var finalOutput strings.Builder + var stderrOutput strings.Builder var parseErr error var resultMsg *StreamMessage @@ -142,10 +143,10 @@ func (a *Adapter) Execute(ctx context.Context, req *domain.AgentRequest, handler resultMsg, parseErr = a.parseStreamOutput(stdout, handler, &finalOutput) }() - // Stream stderr as error events + // Stream stderr as error events and capture for error message go func() { defer wg.Done() - a.streamStderr(stderr, handler) + a.streamStderrCapture(stderr, handler, &stderrOutput) }() wg.Wait() @@ -168,13 +169,17 @@ func (a *Adapter) Execute(ctx context.Context, req *domain.AgentRequest, handler result.ExitCode = 1 result.Error = cmdErr } + // Include stderr and troubleshooting help in error + result.Error = a.buildErrorWithHelp(result.Error, stderrOutput.String(), namespace, podName) } else if parseErr != nil { result.ExitCode = 1 - result.Error = parseErr + result.Error = a.buildErrorWithHelp(parseErr, stderrOutput.String(), namespace, podName) } else if resultMsg != nil && !resultMsg.IsSuccess() { result.ExitCode = 1 if resultMsg.Error != "" { - result.Error = fmt.Errorf("%s", resultMsg.Error) + result.Error = a.buildErrorWithHelp(fmt.Errorf("%s", resultMsg.Error), stderrOutput.String(), namespace, podName) + } else { + result.Error = a.buildErrorWithHelp(nil, stderrOutput.String(), namespace, podName) } } @@ -274,8 +279,8 @@ func (a *Adapter) parseStreamOutput(r io.Reader, handler domain.AgentEventHandle return resultMsg, nil } -// streamStderr reads stderr and emits error events. -func (a *Adapter) streamStderr(r io.Reader, handler domain.AgentEventHandler) { +// streamStderrCapture reads stderr, emits error events, and captures output. +func (a *Adapter) streamStderrCapture(r io.Reader, handler domain.AgentEventHandler, capture *strings.Builder) { scanner := bufio.NewScanner(r) buf := make([]byte, 0, 64*1024) scanner.Buffer(buf, 1024*1024) @@ -292,9 +297,43 @@ func (a *Adapter) streamStderr(r io.Reader, handler domain.AgentEventHandler) { Content: line, Stream: "stderr", }) + + // Capture stderr for error message (limit to 4KB) + if capture.Len() < 4096 { + if capture.Len() > 0 { + capture.WriteString("\n") + } + capture.WriteString(line) + } } } +// buildErrorWithHelp creates an error message with stderr output and troubleshooting help. +func (a *Adapter) buildErrorWithHelp(err error, stderr, namespace, podName string) error { + var msg strings.Builder + + if err != nil { + msg.WriteString(err.Error()) + } else { + msg.WriteString("claude command failed") + } + + // Include stderr if available + if stderr != "" { + msg.WriteString("\n\nstderr:\n") + msg.WriteString(stderr) + } + + // Add troubleshooting help + msg.WriteString("\n\n---\nTroubleshooting:\n") + msg.WriteString("If Claude is not authenticated, run:\n") + fmt.Fprintf(&msg, " kubectl exec -it -n %s %s -- claude login\n", namespace, podName) + msg.WriteString("\nTo test Claude manually:\n") + fmt.Fprintf(&msg, " kubectl exec -it -n %s %s -- claude -p \"hello\"\n", namespace, podName) + + return fmt.Errorf("%s", msg.String()) +} + // Cancel attempts to cancel a running session. func (a *Adapter) Cancel(ctx context.Context, sessionID string) error { a.sessionsMu.Lock()