Skip to content

fix(core): insert content literally into LLM prompts to avoid $ substitution - #27552

Closed
Pluviobyte wants to merge 2 commits into
google-gemini:mainfrom
Pluviobyte:fix/llm-prompt-dollar-substitution
Closed

fix(core): insert content literally into LLM prompts to avoid $ substitution#27552
Pluviobyte wants to merge 2 commits into
google-gemini:mainfrom
Pluviobyte:fix/llm-prompt-dollar-substitution

Conversation

@Pluviobyte

@Pluviobyte Pluviobyte commented May 29, 2026

Copy link
Copy Markdown

Summary

Several LLM prompt builders interpolate user/file content into a template with String.prototype.replace('{placeholder}', value). The replacement argument honors special patterns, so any value containing $ is silently corrupted before being sent to the model. This interpolates each prompt in a single pass with a replacer function, which both inserts content literally and avoids re-interpolating placeholder tokens that appear inside input values.

Details

String.prototype.replace(searchString, replacement) interprets $$, $&, $` and $' in the replacement argument even when the search is a plain string. The affected values are file contents, shell/tool output, and conversation text — which routinely contain $. Concrete corruption (verified):

input fragment becomes
$& the placeholder name, e.g. {current_content}
$$ a single $
$` everything in the prompt before the placeholder (dumps the template)
$' everything after the placeholder (truncates the content)

So e.g. echo $$ > pid.txt is sent to the model as echo $ > pid.txt, and a file containing $` leaks the prompt template into the <file_content> block.

Fix: interpolate in a single pass with a replacer function, e.g.

const userPrompt = EDIT_USER_PROMPT.replace(
  /\{(\w+)\}/g,
  (match, key) => replacements[key] ?? match,
);

The function form inserts $-sequences literally (no GetSubstitution), and the single pass also fixes a sequential-interpolation issue: a placeholder token appearing inside one input value (e.g. an edit parameter that itself contains {current_content}) is no longer re-substituted by a later replacement.

Affected call sites (all fixed):

  • packages/core/src/utils/llm-edit-fixer.ts — edit self-correction prompt
  • packages/core/src/utils/summarizer.ts — tool-output summarizer
  • packages/core/src/services/sessionSummaryService.ts — session summary

(bugCommand.ts uses the same .replace('{x}', value) shape but wraps values in encodeURIComponent, so it is already safe and left unchanged.)

Related Issues

Closes #27556

How to Validate

cd packages/core
npm run typecheck
npx vitest run src/utils/llm-edit-fixer.test.ts src/utils/summarizer.test.ts src/services/sessionSummaryService.test.ts

New tests in llm-edit-fixer.test.ts cover (a) $-content ($&, $$, $`, $') being inserted literally and (b) a placeholder token supplied as input not being re-interpolated. Both fail on the previous code and pass with this change. The pre-existing prompt test only used $-free fixtures, which is why the bug slipped through.

Pre-Merge Checklist

  • Updated relevant documentation and README (not needed — internal behavior fix)
  • Added/updated tests (regression tests for $ sequences and placeholder-in-input)
  • Noted breaking changes (none — prompts now contain the intended literal content)
  • Validated on required platforms/methods:
    • Linux
      • npm run (vitest + tsc --noEmit for @google/gemini-cli-core)

Note: platform-independent string-handling fix covered by automated unit tests, validated via the package test suite on Linux.

…itution

Several LLM prompt builders interpolate user/file content into a template via
`String.prototype.replace('{placeholder}', value)`. The replacement argument
honors special patterns (`$$`, `$&`, `` $` ``, `$'`), so any value containing a
`$` is silently corrupted before being sent to the model:

- `$&`        -> the matched placeholder name itself
- `$$`        -> a single `$`
- `` $` ``/`$'` -> the text before/after the match (can dump the template prefix
  into the content or truncate it)

This affects the edit self-correction prompt (llm-edit-fixer.ts), the tool-output
summarizer (summarizer.ts), and the session summary (sessionSummaryService.ts) —
all of which routinely receive file contents, shell output, or conversation text
that can contain `$`.

Switch these call sites to the existing `safeLiteralReplace` helper
(utils/textUtils.ts), which escapes `$` so the value is inserted verbatim. Add a
regression test covering `$&`, `$$`, `` $` `` and `$'`; it fails before this fix
and passes after.
@Pluviobyte
Pluviobyte requested a review from a team as a code owner May 29, 2026 07:54
@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 addresses a critical bug in how user-provided content is interpolated into LLM prompts. By switching from standard string replacement to a safe literal replacement utility, the changes ensure that special characters are treated as plain text rather than substitution patterns, preventing prompt corruption and potential template leakage.

Highlights

  • Prompt Injection Prevention: Replaced standard String.prototype.replace calls with the safeLiteralReplace helper across multiple prompt-building services to prevent unintended interpretation of '$' sequences.
  • Bug Fix: Resolved an issue where content containing special characters like '$&', '$$', '$`', and '$'' was being corrupted or leaking prompt templates when inserted into LLM prompts.
  • Regression Testing: Added a new test case in llm-edit-fixer.test.ts that explicitly validates the literal insertion of '$' sequences into prompts.
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.

@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 changes to safely handle literal replacements of $-sequences in prompt templates by utilizing a custom safeLiteralReplace helper across several services and utilities, along with adding corresponding unit tests. The reviewer feedback highlights a sequential interpolation vulnerability in llm-edit-fixer.ts where sequential replacements could lead to double-interpolation bugs if user inputs contain placeholder tokens, and suggests using a single-pass regex replacement with a replacer function. Additionally, the reviewer suggests using native String.prototype.replace with a replacer function in sessionSummaryService.ts to avoid the custom helper entirely, as JavaScript natively treats the returned string of a replacer function literally.

Comment on lines +163 to +178
// Use safeLiteralReplace so that `$`-sequences (e.g. `$&`, `$$`, `` $` ``) in
// file content or edit parameters are inserted literally instead of being
// interpreted as String.prototype.replace substitution patterns.
let userPrompt = safeLiteralReplace(
EDIT_USER_PROMPT,
'{instruction}',
instruction,
);
userPrompt = safeLiteralReplace(userPrompt, '{old_string}', old_string);
userPrompt = safeLiteralReplace(userPrompt, '{new_string}', new_string);
userPrompt = safeLiteralReplace(userPrompt, '{error}', error);
userPrompt = safeLiteralReplace(
userPrompt,
'{current_content}',
current_content,
);

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.

security-high high

The current implementation performs sequential replacements using safeLiteralReplace. This introduces a sequential interpolation vulnerability (double-interpolation bug). If any of the user-controlled inputs (such as instruction, old_string, new_string, or error) contain a placeholder string like {old_string} or {current_content}, that placeholder will be replaced in subsequent steps. This can lead to prompt corruption or prompt injection.

To fix this and also completely avoid the $ substitution issue without needing custom helpers, we can perform a single-pass replacement using a native regular expression with a replacer function. Since the replacement argument is a function, special patterns like $ are treated as literal characters natively.

Suggested change
// Use safeLiteralReplace so that `$`-sequences (e.g. `$&`, `$$`, `` $` ``) in
// file content or edit parameters are inserted literally instead of being
// interpreted as String.prototype.replace substitution patterns.
let userPrompt = safeLiteralReplace(
EDIT_USER_PROMPT,
'{instruction}',
instruction,
);
userPrompt = safeLiteralReplace(userPrompt, '{old_string}', old_string);
userPrompt = safeLiteralReplace(userPrompt, '{new_string}', new_string);
userPrompt = safeLiteralReplace(userPrompt, '{error}', error);
userPrompt = safeLiteralReplace(
userPrompt,
'{current_content}',
current_content,
);
const replacements: Record<string, string> = {
instruction,
old_string,
new_string,
error,
current_content,
};
const userPrompt = EDIT_USER_PROMPT.replace(
/\{(\w+)\}/g,
(match, key) => replacements[key] ?? match,
);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — addressed in 7f06728. Replaced the sequential safeLiteralReplace calls with a single-pass EDIT_USER_PROMPT.replace(/\{(\w+)\}/g, (match, key) => replacements[key] ?? match). The replacer function inserts $-sequences literally, and the single pass ensures a placeholder token appearing inside an input value (e.g. old_string === '{current_content}') is no longer re-interpolated by a later step. Added a regression test (does not re-interpolate placeholder tokens that appear inside inputs).

Comment on lines +111 to +115
const prompt = safeLiteralReplace(
SUMMARY_PROMPT,
'{conversation}',
conversationText,
);

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

Instead of importing and using the custom safeLiteralReplace helper, we can use the native String.prototype.replace with a replacer function. When the replacement argument is a function, JavaScript natively treats the returned string as a literal, completely ignoring any special $ substitution patterns. This is more idiomatic, standard, and avoids unnecessary helper imports.

      const prompt = SUMMARY_PROMPT.replace('{conversation}', () => conversationText);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 7f06728 — switched to the native replacer-function form SUMMARY_PROMPT.replace('{conversation}', () => conversationText) and dropped the safeLiteralReplace import (applied the same single-pass replacer approach to summarizer.ts as well for consistency).

@gemini-cli gemini-cli Bot added the status/need-issue Pull requests that need to have an associated issue. label May 29, 2026
Address review feedback on the prompt builders. Replace the sequential
safeLiteralReplace calls with a single-pass `String.prototype.replace` using a
replacer function:

- the function form already inserts `$`-sequences (`$&`, `$$`, `` $` ``, `$'`)
  literally, so no custom helper is needed; and
- a single pass prevents a placeholder token that appears inside one input value
  (e.g. an edit parameter that itself contains the text `{current_content}`) from
  being re-interpolated by a later replacement (a sequential-interpolation bug).

Applies the same approach to llm-edit-fixer, summarizer and sessionSummaryService,
and adds a regression test covering a placeholder token supplied as input.
@Pluviobyte

Copy link
Copy Markdown
Author

/gemini review

@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 fixes template interpolation issues where special $-sequences (such as $& or $$) in user inputs or conversation texts were incorrectly treated as substitution patterns by String.prototype.replace. The fix introduces replacer functions and single-pass regex replacements with a lookup dictionary in sessionSummaryService.ts, llm-edit-fixer.ts, and summarizer.ts. This single-pass approach also prevents nested placeholder tokens from being re-interpolated. Corresponding unit tests have been added to verify the correct behavior. I have no feedback to provide as the changes are well-implemented and include appropriate test coverage.

@gemini-cli gemini-cli Bot added priority/p2 Important but can be addressed in a future release. area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality and removed status/need-issue Pull requests that need to have an associated issue. labels May 29, 2026
@github-actions github-actions Bot added the size/m A medium sized PR label Jun 2, 2026
@gemini-cli

gemini-cli Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

@Pluviobyte

Copy link
Copy Markdown
Author

Thanks for the intake-policy nudge. I added the requested source/version context on the linked issue (#27556) on 2026-05-30. The later Gemini Code Assist feedback was addressed in 7f06728, the old review threads are outdated, and the follow-up Gemini review reported no further feedback.

If maintainers are open to accepting this fix, could #27556 be considered for help wanted? Otherwise I understand that the PR may be closed under the current intake policy.

@gemini-cli

gemini-cli Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

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/p2 Important but can be addressed in a future release. size/m A medium sized PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: LLM prompt builders corrupt content containing $ sequences

1 participant