Skip to content

fix(shell): bound command output sent to the model#28401

Open
enjoykumawat wants to merge 1 commit into
google-gemini:mainfrom
enjoykumawat:fix/28090-large-shell-output
Open

fix(shell): bound command output sent to the model#28401
enjoykumawat wants to merge 1 commit into
google-gemini:mainfrom
enjoykumawat:fix/28090-large-shell-output

Conversation

@enjoykumawat

Copy link
Copy Markdown
Contributor

Summary

The shell tool forwards the entire command output to the model with no upper bound. A single command that prints a lot (e.g. find /, a verbose build, a large git log) can inject hundreds of KB into the model context, degrading responses and burning tokens. This PR bounds the output sent to the model at 32 KiB while leaving the full output available to the user-facing display path.

Details

  • Added truncateLlmOutput() in packages/core/src/tools/shell.ts: keeps the head and tail of the output (head-biased split) and inserts a marker line stating how many bytes were elided, so the model still sees how the command started and finished.
  • Truncation is byte-bounded (MAX_LLM_OUTPUT_BYTES = 32 KiB) and codepoint-safe — it never splits a multi-byte UTF-8 character.
  • Applied to both the normal-completion path and the aborted-command path, since both previously built llmContent from the full result.output.
  • Only llmContent is affected; returnDisplay (what the user sees) is unchanged.
  • The existing opt-in summarizeToolOutput mediation still applies; this is a hard backstop when summarization is not configured.

Related Issues

Fixes #28090

How to Validate

  1. cd packages/core && npx vitest run src/tools/shell.test.ts — 93 tests pass, including 4 new ones covering: output under the limit passes through untouched, oversized output is bounded with the truncation marker, multi-byte characters at the cut boundary are not split, and the aborted-command path is also bounded.
  2. Manual repro from the issue: run a command producing >32 KiB (e.g. node -e "console.log('x'.repeat(40319))") through the shell tool and inspect llmContent — it is capped at 32768 bytes with head and tail preserved (the reported 40319-byte case truncates to 32768).

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any) — none
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

The shell tool forwarded the full command output to the provider in its
tool result. The only mediation was summarizeToolOutput, which is disabled
by default, so a command printing a large payload sent it back unbounded
(issue google-gemini#28090 observed a 40319-byte tool result).

Add truncateLlmOutput: a deterministic, codepoint-safe head+tail byte bound
(MAX_LLM_OUTPUT_BYTES, 32 KiB) applied to the output segment of the tool
result on normal completion and on cancellation-with-output. Error/exit-code
lines are unaffected since they are appended after the output. Output within
the bound is returned unchanged; enabling tool-output summarization remains
the smarter, model-driven alternative.
@enjoykumawat
enjoykumawat requested a review from a team as a code owner July 14, 2026 10:16
@github-actions github-actions Bot added the size/m A medium sized PR label Jul 14, 2026
@github-actions

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 98
  • Additions: +96
  • Deletions: -2
  • Files changed: 2

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a safety mechanism to limit the amount of shell command output forwarded to the model. By enforcing a 32 KiB limit, it prevents excessively large outputs from overwhelming the model's context window, while ensuring that critical diagnostic information at the beginning and end of the output remains available. The user-facing display remains unaffected, preserving the full output for the user.

Highlights

  • Output Truncation: Implemented a 32 KiB byte-bound limit on shell command output sent to the LLM to prevent context window degradation and token wastage.
  • Head-Biased Truncation Strategy: Introduced a truncation mechanism that preserves the start and end of the command output, inserting a marker to indicate where data was elided.
  • UTF-8 Safety: Ensured that truncation logic is codepoint-safe, preventing the splitting of multi-byte UTF-8 characters.
  • Test Coverage: Added comprehensive unit tests covering standard output, oversized output, multi-byte character handling, and aborted command scenarios.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions

Copy link
Copy Markdown

🛑 Action Required: Evaluation Approval

Steering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged.

Maintainers:

  1. Go to the Workflow Run Summary.
  2. Click the yellow 'Review deployments' button.
  3. Select the 'eval-gate' environment and click 'Approve'.

Once approved, the evaluation results will be posted here automatically.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to truncate shell command output sent to the LLM to prevent sending arbitrarily large payloads. It defines a maximum byte limit of 32KB and implements a truncation utility that preserves the head and tail of the output. Feedback was provided regarding potential performance and memory issues when calling Array.from() on large command outputs, suggesting the use of Intl.Segmenter to safely process grapheme clusters instead.

Comment on lines +95 to +99
function takeBytes(output: string, maxBytes: number, fromEnd: boolean): string {
if (maxBytes <= 0) {
return '';
}
const chars = Array.from(output);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling Array.from(output) on the entire, potentially huge command output can cause severe performance degradation or Out-Of-Memory (OOM) crashes. However, using string.slice() directly to truncate the string before operating on grapheme clusters can split multi-byte Unicode characters (such as emojis or surrogate pairs) at the boundary. To prevent character splitting while maintaining performance, we should use Intl.Segmenter to safely iterate over grapheme clusters up to the limit without loading the entire array into memory.

function takeBytes(output: string, maxBytes: number, fromEnd: boolean): string {
  if (maxBytes <= 0) {
    return '';
  }
  const segmenter = new Intl.Segmenter();
  if (fromEnd) {
    const safeSlice = output.slice(-maxBytes * 2);
    const segments = Array.from(segmenter.segment(safeSlice));
    return segments.slice(-maxBytes).map(s => s.segment).join('');
  } else {
    const segments = segmenter.segment(output);
    let result = '';
    let count = 0;
    for (const { segment } of segments) {
      if (count >= maxBytes) break;
      result += segment;
      count++;
    }
    return result;
  }
}
References
  1. When truncating strings that may contain multi-byte Unicode characters (e.g., emojis), use methods that operate on grapheme clusters (like Intl.Segmenter or Array.from()) instead of UTF-16 code units (string.length, string.slice()) to prevent character splitting.

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

Labels

area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality priority/p1 Important and should be addressed in the near term. size/m A medium sized PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gemini CLI sends large shell output back to the provider

1 participant