Skip to content
| Marketplace
Sign in
Visual Studio Code>Other>LLM Save MergeNew to Visual Studio Code? Get it now.
LLM Save Merge

LLM Save Merge

Daniel Hails

| (0) | Free
When a file changes on disk underneath an unsaved editor buffer, 3-way merge the two versions with an LLM instead of surfacing VS Code's save-conflict dialog.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

LLM Save Merge

When a file changes on disk underneath an unsaved (dirty) editor buffer — a parallel agent session, a formatter, a git operation — VS Code's only offer at save time is the "content of the file is newer" dialog: Overwrite or Compare. This extension removes that fork: the moment disk diverges under a dirty buffer, it runs a 3-way LLM merge (base / your buffer / disk) in the background and swaps the merged result into your buffer, re-based on the new disk state. By the time you save, there is no conflict.

Behaviour

  • Trigger: a watched file changes on disk while its buffer is dirty. Three detection paths back each other up: file-watcher events; a one-shot disk check when a document is first watched (catches divergence from before watching began, e.g. a dirty buffer restored from backup); and a 2-second mtime poll over watched dirty buffers, because VS Code's workspace file watcher can die — a directory deleted mid-scan kills it for the whole window — and it never restarts. Merges are debounced and re-checked for staleness (buffer or disk moving during the merge retries the merge, up to twice).
  • Mechanical first, model for disputes only: the two sides' edits are diffed against the ancestor as hunks, and any hunk that touches no hunk of the other side (diff3's rule: an unchanged line must separate them) is applied directly — as is a hunk both sides made identically. When nothing is disputed, that is the merge: no model call, milliseconds, byte-identical to git merge-file (the test suite checks this against git on seeded random edit pairs). Every merge this extension had ever journalled was of that kind, and had been costing 30 s to 5 min of model time.
  • Excerpt-scoped prompts for what is left: when the sides do dispute a region, the model receives the three versions with every settled hunk already applied, cut down to the excerpt covering the disputed hunks (plus a few context lines) and two unified diffs — original→buffer and original→disk — and returns only the merged excerpt, which is spliced back into the file mechanically. Before any model call, a runtime invariant checks the splice arithmetic: each side's own segment spliced into the original must reproduce that side byte-for-byte. Unknown ancestors, disputes spanning most of the file, a failed invariant, or a rejected excerpt (edge context lines must survive verbatim) fall back to the full three-version prompt. The excerpt's edges always sit on non-blank lines — models trim blank lines from the ends of their output, and a blank edge anchor would send every such merge to the fallback.
  • A lean model call: claude runs in --bare mode with no tools, MCP servers, hooks, plugins, or CLAUDE.md — a merge needs none of them, and together they had cost ~13 s and a 54k-token system prompt per call.
  • 3-way, not 2-way: the extension snapshots the disk content each buffer is based on (at open, save, and reload), so the model sees the common ancestor. Incompatible collisions prefer your buffer; disk-only changes are never dropped.
  • Progress you can see and stop: a model call takes tens of seconds, so a merge shows a notification with the merge phase and elapsed time, plus a Cancel button that kills the model call and leaves the buffer untouched (fail-open, as ever).
  • Stale calls die immediately: if the buffer is edited or the disk changes again while a model call is in flight, the call is killed on the spot — its result could only be discarded — and the merge reschedules with fresh inputs.
  • Disk is never written: only the buffer is updated. The file on disk changes when you save, as usual.
  • Every merge is journalled to ~/.local/state/llm-save-merge/<timestamp>-<file>/ (base, ours, theirs, merged), and the notification offers a disk↔merged diff.
  • Fail open: timeouts, oversized files (> maxFileBytes), or rejected model output leave the buffer untouched, so VS Code's normal conflict dialog still protects you.

Requirements

The claude CLI must be installed on the machine where the extension host runs (the remote, for Remote-SSH), and an ANTHROPIC_API_KEY must reach it: merges run claude --bare, which authenticates with the API key only and never reads the ~/.claude OAuth login.

A key for the extension host, not just for your terminal. The extension host runs with the VS Code server's login environment. Anything an interactive shell exports on top of that — an ANTHROPIC_API_KEY sourced by a shell function, say — never reaches the claude child. Point llmSaveMerge.envFile at a dotenv file exporting the key your shell uses (re-read on every merge), or export it in the login environment itself.

Settings

setting default meaning
llmSaveMerge.enabled true auto-merge on disk divergence
llmSaveMerge.model claude-sonnet-5-5 model id for claude --print --bare --model
llmSaveMerge.claudePath claude claude binary (~/.local/bin is added to PATH)
llmSaveMerge.timeoutMs 240000 merge call timeout (full-file merges of large files are output-bound and slow)
llmSaveMerge.maxFileBytes 400000 skip files larger than this
llmSaveMerge.envFile "" dotenv file added to the claude child's environment (~ expands; export prefix and quotes allowed)

The command palette entry LLM Save Merge: Merge disk changes into active file runs a merge on demand (works even with enabled off). For a buffer that was restored dirty from backup — where the true ancestor is unknowable and disk divergence from before the restore is invisible to the automatic triggers — the manual command merges anyway, ancestor-less: the model combines buffer and disk faithfully.

When a merge fails

Every failure shows a warning with the reason and a Show log button; the LLM Save Merge output channel carries the full record: the spawned command line and which credentials the child saw (names only, never values), the exit code or signal, and both stdout and stderr. claude --print reports authentication errors on stdout (observed on 2.1.259), so an "exited 1" with an empty stderr has its message there.

An authentication failure (Not logged in, OAuth session expired, invalid api key, …) gets a dialog naming the fix — set envFile to a dotenv exporting ANTHROPIC_API_KEY — plus an Open settings button, and pauses automatic merges for 10 minutes so each disk change does not re-fail and re-prompt. The pause lifts on any llmSaveMerge settings change, on the first successful merge, and never applies to the palette command.

Tests

npm test                          # offline: mechanical merge vs git merge-file, span arithmetic, fallbacks, failure paths, stubbed CLI
npm run test:live                 # live: real claude CLI — a mechanical merge, both prompt shapes
xvfb-run -a npm run test:e2e      # e2e: real VS Code, stubbed CLI, conflict-free save

The offline suite includes a seeded oracle test: 300 random edit pairs merged by the mechanical stage and by git merge-file, asserting the same clean/disputed verdict on every pair and identical text on every clean one (skipped when git is absent).

The e2e run downloads a VS Code build to .vscode-test/ on first use and drives the real extension in a temp workspace (stub CLI, replies routed by <file_path>). Every merging scenario is a genuine dispute — disk rewrites the line the buffer edited — plus a disk-only edit the mechanical stage settles, since disjoint edits never reach the stub. Six scenarios, one instance: the basic dirty-buffer auto-merge ending in a conflict-free save; a corrupted excerpt reply falling back to the full-file prompt; a clean-buffer auto-reload followed by a merge that must use the reloaded base; an mtime-only touch that must stay quiet; the palette command merging on demand with auto-merge disabled; and a buffer edit mid-call that must kill the call and re-merge. On failure the suite prints every document event it saw.

Build & install

npx --yes @vscode/vsce package --allow-missing-repository
~/.vscode-server/cli/servers/Stable-*/server/bin/code-server --install-extension llm-save-merge-*.vsix

Then reload the VS Code window.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft