From 8246ffa3929d90231cc3059e094c82755169373e Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Fri, 26 Jun 2026 05:47:52 +0800 Subject: [PATCH] fix: prevent null output and string render crashes in MessagesBoundary UserToolSuccessMessage now requires parsedOutput.success before trusting data, and guards toolResult against non-object values before calling renderToolResultMessage. String renderedMessage is wrapped in so multi-line tool reports (e.g. GoalTool usage report) don't crash Ink. Defense in depth added to VaultHttpFetchTool/UI (matches the existing pattern in LocalMemoryRecallTool and GoalTool) and to the mapToolResultToToolResultBlockParam of both vault/memory tools. Co-Authored-By: glm-5.2 --- package.json | 2 +- .../builtin-tools/src/tools/GoalTool/GoalTool.ts | 5 ++++- .../LocalMemoryRecallTool.ts | 2 +- .../src/tools/VaultHttpFetchTool/UI.tsx | 3 +++ .../VaultHttpFetchTool/VaultHttpFetchTool.ts | 2 +- .../UserToolSuccessMessage.tsx | 16 ++++++++++++++-- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 57eb8338b..d46fe9b82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-best", - "version": "2.8.1", + "version": "2.8.2", "description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal", "type": "module", "author": "claude-code-best ", diff --git a/packages/builtin-tools/src/tools/GoalTool/GoalTool.ts b/packages/builtin-tools/src/tools/GoalTool/GoalTool.ts index 742b99d32..0b413292b 100644 --- a/packages/builtin-tools/src/tools/GoalTool/GoalTool.ts +++ b/packages/builtin-tools/src/tools/GoalTool/GoalTool.ts @@ -137,7 +137,10 @@ export const GoalTool = buildTool({ return `Updating goal: ${input.status}${input.reason ? ` — ${input.reason}` : ''}` }, renderToolResultMessage(output: Output) { - if (output.error) return `Goal error: ${output.error}` + if (!output) { + return null + } + if (output?.error) return `Goal error: ${output.error}` if (output.report) return output.report if (output.goal) { return `Goal "${output.goal.objective}" — ${output.goal.status}` diff --git a/packages/builtin-tools/src/tools/LocalMemoryRecallTool/LocalMemoryRecallTool.ts b/packages/builtin-tools/src/tools/LocalMemoryRecallTool/LocalMemoryRecallTool.ts index 64cbcabaf..2f11a044e 100644 --- a/packages/builtin-tools/src/tools/LocalMemoryRecallTool/LocalMemoryRecallTool.ts +++ b/packages/builtin-tools/src/tools/LocalMemoryRecallTool/LocalMemoryRecallTool.ts @@ -547,7 +547,7 @@ export const LocalMemoryRecallTool = buildTool({ type: 'tool_result', tool_use_id: toolUseID, content: jsonStringify(output), - is_error: output.error !== undefined, + is_error: output?.error !== undefined, } }, } satisfies ToolDef) diff --git a/packages/builtin-tools/src/tools/VaultHttpFetchTool/UI.tsx b/packages/builtin-tools/src/tools/VaultHttpFetchTool/UI.tsx index 7c99385b4..8ee278cbe 100644 --- a/packages/builtin-tools/src/tools/VaultHttpFetchTool/UI.tsx +++ b/packages/builtin-tools/src/tools/VaultHttpFetchTool/UI.tsx @@ -33,6 +33,9 @@ export function renderToolResultMessage( _progressMessagesForMessage: ProgressMessage[], { verbose }: { verbose: boolean }, ): React.ReactNode { + // Defense in depth: framework validates via outputSchema, but resumed + // transcripts can still produce null here via deserialization edge cases. + if (!output) return null; if (output.error) { return ( diff --git a/packages/builtin-tools/src/tools/VaultHttpFetchTool/VaultHttpFetchTool.ts b/packages/builtin-tools/src/tools/VaultHttpFetchTool/VaultHttpFetchTool.ts index 1badcf802..2b549447f 100644 --- a/packages/builtin-tools/src/tools/VaultHttpFetchTool/VaultHttpFetchTool.ts +++ b/packages/builtin-tools/src/tools/VaultHttpFetchTool/VaultHttpFetchTool.ts @@ -409,7 +409,7 @@ export const VaultHttpFetchTool = buildTool({ type: 'tool_result', tool_use_id: toolUseID, content: jsonStringify(output), - is_error: output.error !== undefined, + is_error: output?.error !== undefined, } }, } satisfies ToolDef) diff --git a/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx b/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx index d595ca0bc..bada2d9b5 100644 --- a/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +++ b/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx @@ -67,7 +67,14 @@ export function UserToolSuccessMessage({ if (parsedOutput && !parsedOutput.success) { return null; } - const toolResult = parsedOutput?.data ?? message.toolUseResult; + // Only trust schema-validated output. Fall back to raw toolUseResult only + // when it's a non-null object — schemas without outputSchema, or successful + // parses that yield null/undefined data, must not reach renderToolResultMessage + // (tool UIs access output.error / output.action on first line and crash). + const toolResult = parsedOutput?.success ? parsedOutput.data : message.toolUseResult; + if (!toolResult || typeof toolResult !== 'object') { + return null; + } // Collapse diff display for old messages (verbose/ctrl+o overrides) const effectiveStyle = shouldCollapseDiffs && !verbose ? 'condensed' : style; @@ -88,6 +95,11 @@ export function UserToolSuccessMessage({ return null; } + // Ink requires text strings to be inside . Tools that return plain + // multi-line strings (e.g. GoalTool's usage report) crash without the wrap. + // React elements from UI.tsx files pass through unchanged. + const wrappedMessage = typeof renderedMessage === 'string' ? {renderedMessage} : renderedMessage; + // Tools that return '' from userFacingName opt out of tool chrome and // render like plain assistant text. Skip the tool-result width constraint // so MarkdownTable's SAFETY_MARGIN=4 (tuned for the assistant-text 2-col @@ -97,7 +109,7 @@ export function UserToolSuccessMessage({ return ( - {renderedMessage} + {wrappedMessage} {feature('BASH_CLASSIFIER') ? classifierRule && (