Skip to content

[Bug] Replacement strings containing $ corrupt every string_replace and diff_edit #1057

Description

@addyCooks

Description

string_replace and diff_edit pass the model's replacement text as the second
argument to String.prototype.replace. That argument is not a literal the
engine runs GetSubstitution over it, so four token sequences are rewritten
before the bytes reach disk:

$$   ->  collapses to a single "$"
$&   ->  injects the matched text
$`   ->  injects everything BEFORE the match
$'   ->  injects everything AFTER the match

These are ordinary characters in shell scripts, Makefiles, docker-compose files,
CI YAML, and any code that builds regexes. The file silently ends up containing
something the model never asked for and the user never approved.

The $` and $' forms are the damaging ones: each duplicates a whole half of the file
into the middle of the edit, so corruption scales with file size.

Environment

  • OS: Windows 11 (language-level; reproduces on every platform)
  • Node version: v22.22.3
  • Nanocoder version: 1.30.0
  • Provider: any
  • Model: any

Steps to Reproduce

  1. read_file a shell script so the read-tracker marks it seen.
  2. Call string_replace on it with a new_str containing any of the four tokens
    (the reduced case below uses all four).
  3. Compare the approved preview with the bytes on disk.

Reduced to the one line that matters:

const file    = '#!/bin/sh\necho "old"\nexit 0\n';
const old_str = 'echo "old"';
const new_str = 'echo "pid=$$ match=$& pre=$` post=$\'"';

file.replace(old_str, new_str);        // what the tool does
file.replace(old_str, () => new_str);  // what it should do

Expected Behavior

The file contains exactly the new_str the model supplied, byte for byte.

Actual Behavior

requested new_str : "echo \"pid=$$ match=$& pre=$` post=$'\""
current  behaviour: "#!/bin/sh\necho \"pid=$ match=echo \"old\" pre=#!/bin/sh\n post=\nexit 0\n\"\nexit 0\n"
with fn replacer  : "#!/bin/sh\necho \"pid=$$ match=$& pre=$` post=$'\"\nexit 0\n"

The tool reports success. Nothing warns the model or the user.

Logs/Screenshots

Affected call sites:

  • source/tools/file-ops/string-replace.tsx:91 fileContent.replace(old_str, new_str), the write path
  • source/tools/file-ops/diff-edit.tsx:165 newContent.replace(block.search, block.replace), once per block
  • source/tools/file-ops/string-replace.tsx:165 the same call in the VS Code diff formatter

Additional Context

The approval gate is bypassed by construction rather than by attack: the terminal
confirmation renders old_str / new_str directly, so the diff the user approves
is not the diff that lands.

Suggested fix — use a function replacer at both write sites, so no substitution
parsing happens at all:

const newContent = fileContent.replace(old_str, () => new_str);

Splicing by index works too and avoids re-scanning the string. Line 165 of
string-replace.tsx should get the same treatment so the VS Code preview keeps
matching what is written. Worth a regression test whose new_str carries all
four tokens.

  • I have searched existing issues to ensure this is not a duplicate
  • I can reproduce this issue consistently
  • This issue occurs with the latest version of nanocoder

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions