Skip to content

edit_existing_file silently performs full-file overwrite when changes payload has no anchor markers #13307

Description

@jattie-ire

Before submitting your bug report

Relevant environment info

- OS:Windows 11 Enterprise
- Continue version: 2.0.0
- IDE version: 1.138.0
- Model: Qwen3.8-27B
- config:
  
name: Main Config
version: 1.0.0
schema: v1
models:
  - name: Qwen-Coder
    provider: openai
    model: Qwen-Coder
    apiBase: http://172.19.128.237:8000/v1
    apiKey: EMPTY
    capabilities:
      - tool_use
      - image_input
    roles:
      - chat
      - edit
      - apply
      - autocomplete
    defaultCompletionOptions:
      temperature: 1.0
      top_p: 0.95
      maxTokens: 8192
      contextLength: 131072
      reasoning: true
    requestOptions:
      stream: false
      extraBodyProperties:
        top_k: 20
        min_p: 0.0
        presence_penalty: 0.0
        repetition_penalty: 1.0
        reasoning_effort: "low" # "medium" "high"

  
  OR link to agent in Continue hub:

Description

OS: Linux
Model/provider (repro): any agent-mode model via an OpenAI-compatible provider (repro'd with Qwen-Coder over vLLM)

Summary
When the agent calls the built-in edit_existing_file tool and the changes argument is a bare changed line (no // ... existing code ... anchors, no surrounding unchanged context), Continue treats that text as the entire new file and overwrites the target. The resulting diff shows the whole file deleted and a single line inserted. It returns Edit Success with no warning. Accepting it destroys the file.

Steps to reproduce

  1. In agent mode, request a one-line fix to a file larger than a few lines (e.g. fix a missing function argument).
  2. The model emits edit_existing_file with only the changed line, e.g.:
    {"filepath": "word_generator.py",
     "changes": "    words = find_words(letters, trie, args.min_freq, max_len, args.min_len)"}
  3. Continue renders a diff of delete all N lines → insert 1 line.
  4. Click Accept → the file is reduced to that single line.

Actual behavior

  • Anchor-less changes payload ⇒ whole-file replacement.
  • No validation, no warning, no confirmation.
  • single_find_and_replace on the same file is unaffected: it does exact string substitution and snapshots editingFileContents/newFileContents in processedArgs, producing a correct one-line diff.

Expected behavior
When changes contains no anchor markers (// ... existing code ... etc.) and the target file is large, Continue should refuse the edit or force an explicit confirmation rather than defaulting to full-file overwrite. A bare snippet must never be accepted as the complete new file content without guardrails.

Root cause (from session log 38253ff2-3389-49f3-9b95-5fa12aa37c63.json)

  • Trigger (model layer, outside Continue): model emitted a bare changes line with no anchors. The tool schema documents anchors as the mechanism for reconstructing the kept portions of the file.
  • Amplifier (Continue tool layer, the bug): edit_existing_file has no match; with nothing to anchor against it falls back to "provided text == entire file" and overwrites. This unsafe fallback is what makes a formatting slip data-destructive.
  • Exonerated: the inference/transport provider (vLLM) parsed the tool call correctly and delivered the model output intact; the overwrite is decided entirely by Continue's tool implementation. No inference-side config can fix it.

Proposed fix

  1. In edit_existing_file, detect anchor-less changes on a non-trivial file and reject with a descriptive error (or require an explicit user confirmation) instead of full-replacing.
  2. At minimum, surface a warning on the diff when a proposed edit removes > ~50% of the file.

To reproduce

Steps to reproduce (Continue agent mode, any model via an OpenAI-compatible provider)

Precondition: a workspace file that is substantially longer than the edit — e.g. word_generator.py (~186 lines) containing a one-arg bug:

words = find_words(letters, trie, args.min_freq, max_len)   # missing args.min_len
  1. Open Continue in agent mode, with the terminal showing a traceback pointing at that line.
  2. Send a message that (a) includes the terminal error as context and (b) frames it as a small targeted fix, e.g. "fix the TypeError shown in the terminal."
  3. The agent calls read_file on word_generator.py, then calls edit_existing_file with a bare, un-anchored changes payload (the model does not emit // ... existing code ... or surrounding context):
    {"filepath": "word_generator.py",
     "changes": "    words = find_words(letters, trie, args.min_freq, max_len, args.min_len)"}
  4. Continue returns Edit Success and renders a diff showing the entire file deleted and one line inserted (the full-file overwrite).
  5. Click Accept on that diff.

Result: word_generator.py is reduced to the single line — the rest of the file is deleted. No warning was shown before Accept.

Determining factor (why it's the tool, not the model):
Repeat the identical one-line change using single_find_and_replace with old_string → new_string. It produces a correct one-line diff and preserves the file. The difference is that edit_existing_file, given an anchor-less payload, silently defaults to full-file replacement, whereas single_find_and_replace is exact-match and cannot.

Minimal trigger condition: any edit_existing_file call where changes contains no anchor markers and no surrounding unchanged lines, applied to a file longer than the snippet.

Log output

Here are the exact fragments from `~/.continue/sessions/38253ff2-3389-49f3-9b95-5fa12aa37c63.json` that establish the root cause. I'm quoting them verbatim from the log (I re-read the file; the two decisive records are the `toolCallStates` entries for tool ids `chatcmpl-tool-aee29c1bd61b73c4` and `chatcmpl-tool-96613c2d7465de2b`).

### 1. The destructive call — `edit_existing_file` (tool id `chatcmpl-tool-aee29c1bd61b73c4`)

The model's emitted payload (`parsedArgs`) is a **bare one-line `changes` with no anchors**:

"parsedArgs": {
  "filepath": "word_generator.py",
  "changes": "    words = find_words(letters, trie, args.min_freq, max_len, args.min_len)"
}


Note there is **no** `processedArgs` block with `editingFileContents`/`newFileContents` here — unlike the safe tool below, Continue did **not** snapshot the full before/after, because it treated the bare line as the whole new file. The result:

"output": [
  {
    "name": "Edit Success",
    "content": "Successfully edited file:///home/u121988/continue_coder_test/word_generator.py",
    "description": "",
    "hidden": true
  }
]


### 2. The contrasting safe call — `single_find_and_replace` (tool id `chatcmpl-tool-96613c2d7465de2b`)

This one **does** carry the full snapshots in `processedArgs`, proving the tool layer *can* and *did* reconstruct the file correctly when it isn't forced to guess:

"processedArgs": {
  "filepath": "word_generator.py",
  "old_string": "    words = find_words(letters, trie, args.min_freq, max_len)",
  "new_string": "    words = find_words(letters, trie, args.min_freq, max_len, args.min_len)",
  "fileUri": "file:///home/u121988/continue_coder_test/word_generator.py",
  "editingFileContents": "#!/usr/bin/env python3\n\"\"\"Generate all valid words ...",   // full ~186-line original
  "newFileContents": "#!/usr/bin/env python3\n\"\"\"Generate all valid words ..."          // full file, only line 154 differs
}


"output": [
  { "name": "Edit Success",
    "content": "Successfully edited file:///home/u121988/continue_coder_test/word_generator.py" }
]


### 3. The model output as delivered by the transport (proves vLLM is not the culprit)

From `promptLogs[0].completion`, the streamed tool-call tokens show vLLM delivered **exactly** the bare line the model produced — it neither anchored nor overwrote anything:

<assistant>
edit_existing_file(undefined)
<assistant>
undefined({"filepath": ")
...
undefined(    words =)
...
undefined( find_words()
...
undefined(.min_len))
<assistant>
undefined("})

Reassembled, this is precisely the payload in section 1. The transport parsed and streamed it faithfully; the overwrite was decided downstream by Continue.

---

### What this proves, categorically

- **Trigger (model layer):** `changes` was a bare line with no `// ... ... existing code ...` anchors and no surrounding unchanged context (section 1).
- **Amplifier / bug (Continue tool layer):** `edit_existing_file`, given that anchor-less payload, had nothing to match, so it defaulted to "provided text == entire new file" — hence the whole-file deletion in your screenshot — and returned `Edit Success` with **no full-file snapshot recorded** and **no warning** (section 1).
- **Control (proves it's tool-specific, not model-specific):** the *same* logical edit via `single_find_and_replace` recorded complete `editingFileContents`/`newFileContents` and produced a correct one-line diff (section 2).
- **Transport exonerated:** vLLM delivered the model's output intact and correctly parsed the tool call (section 3). The destructive decision is made entirely in Continue's `edit_existing_file` implementation.

These are the three fragments to paste under the **Root cause** section of the issue.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions