[BUG] `Read` tool returns "file unchanged" after external on-disk edits, serving a stale cached snapshot

Status Closed — not planned
Reported on v2.1.144
Maintainer reply None cached
Activity 8 comments · opened May 19, 2026 · closed Jun 19, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

The Read tool suppresses re-reads with the message:

Wasted call — file unchanged since your last Read. Refer to that earlier tool_result instead.

This freshness check appears to track only edits made by the assistant's own tools (Edit, Write, NotebookEdit), not modifications to the file on disk from other sources (IDE save, another terminal, another process). When a file is edited externally between two Read calls, the second Read falsely reports the file as unchanged and refuses to return current content, causing the assistant to operate on a stale view.

This is inconsistent with the Edit tool, which the tools reference documents as performing a real disk-state check:

Read-before-edit: Claude must have read the file in the current conversation, and the file must not have changed on disk since that read.

The Read tool section does not document any "unchanged" suppression behavior at all, so the message is also undocumented.

Environment

  • Claude Code VSCode native extension, version 2.1.144
  • macOS (Darwin 25.5.0)
  • Model: claude-opus-4-7 (Opus 4.7, 1M context)

Where this was observed

Reproduced inside the Claude Code VSCode native extension (v2.1.144). The Read tool itself is a built-in Claude Code tool shared across surfaces (CLI, IDE extensions), so I'd expect the same staleness suppression to fire in the CLI whenever a file is modified by an external writer (another terminal, a git pull, a formatter, a second agent) between two Read calls — but I haven't verified that. Worth confirming during triage.

Actual

Read returns the "unchanged" message based on the assistant's own edit history only, ignoring external writes.

Notes / things worth checking

  • Whether the suppression keys on path only, or path + something stat-based that's missing the invalidation.
  • Whether VSCode's notebook save (atomic write-and-rename) interacts oddly with mtime-based checks.
  • Whether NotebookEdit properly updates whatever staleness-tracking state Read consults (in my case the external edits happened after NotebookEdit, so this is a separate variable, but worth isolating).
  • A minimal repro should be possible with a plain .txt file edited via echo > file from another terminal between two Read calls.

Workaround

Use Bash (cat, jq, etc.) to re-read a file when it may have been edited outside the current Claude session.

What Should Happen?

Read should stat the file (mtime, size, or content hash) and return fresh content whenever the on-disk state differs from the cached snapshot — matching Edit's read-before-edit invariant.

Error Messages/Logs

`Wasted call — file unchanged since your last Read. Refer to that earlier tool_result instead.`

Steps to Reproduce

  1. Assistant Reads a Jupyter notebook (returns content).
  2. Assistant makes several NotebookEdit calls against specific cell_ids in that file.
  3. User edits the same notebook in the VSCode notebook editor (reorders cells, deletes a cell, changes cell text). File is saved to disk.
  4. Assistant calls Read on the same path → returns the "Wasted call — file unchanged" string instead of fresh content.
  5. Assistant runs cat … | jq via Bash on the same path → returns the actually current on-disk content, confirming the file had changed and Read was wrong.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

Claude Code VSCode native extension, version 2.1.144

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

VS Code integrated terminal

Additional Information

_No response_

View original on GitHub ↗

8 Comments

github-actions[bot] · 4 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/45748

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

jshaofa-ui · 4 months ago

claude-code #60684: Read Tool Returns Stale Cached Snapshot After External Edits

Issue: https://github.com/anthropics/claude-code/issues/60684
Type: Bug — Data Staleness (area:tools, has repro, platform:macos, platform:vscode)
Severity: HIGH — causes assistant to operate on stale data
Quote: $2,000–$3,000

Root Cause Analysis

The Read tool implements a "file unchanged" suppression that tracks only the assistant's own tool operations (Edit, Write, NotebookEdit) but does NOT track external file modifications. This creates a staleness gap when files are modified by external sources between two Read calls.

Technical Analysis

The current freshness check mechanism:

Read #1: file.txt → returns content, records timestamp/hash
  ↓
External edit: IDE saves file.txt (or another process modifies it)
  ↓
Read #2: file.txt → checks "did my tools modify this file?"
  → No (external edit not tracked)
  → Returns: "file unchanged since your last Read"
  → Assistant operates on STALE data from Read #1

Inferred Implementation

// Pseudocode of current behavior (buggy)
class ReadTool {
  private readCache: Map<string, { content: string, hash: string, timestamp: number }>;
  private modifiedByTools: Set<string>; // Only tracks Edit/Write/NotebookEdit
  
  read(filePath: string): ReadResult {
    if (this.modifiedByTools.has(filePath)) {
      // File was modified by our tools, must re-read
      return this.readFromFile(filePath);
    }
    
    const cached = this.readCache.get(filePath);
    if (cached) {
      // BUG: Only checks if our tools modified it, not disk state
      return { suppressed: true, message: "file unchanged since your last Read" };
    }
    
    return this.readFromFile(filePath);
  }
}

Why Edit Tool Works Correctly

The Edit tool performs a real disk-state check because it needs to verify the file hasn't changed since the last read (to prevent edit conflicts). This check uses file system metadata (mtime, inode) rather than just tracking tool operations.

Proposed Fix

Fix 1: Real Disk-State Check in Read Tool

// src/tools/readTool.ts
interface FileState {
  content: string;
  mtime: number;      // File modification time
  size: number;       // File size
  inode?: number;     // Inode (Unix)
}

class ReadTool {
  private fileStates: Map<string, FileState>;
  
  read(filePath: string): ReadResult {
    const resolvedPath = path.resolve(filePath);
    const cached = this.fileStates.get(resolvedPath);
    
    try {
      const stats = fs.statSync(resolvedPath);
      
      if (cached) {
        // Check if file has changed on disk (not just via our tools)
        const hasChanged = stats.mtimeMs !== cached.mtime || 
                           stats.size !== cached.size;
        
        if (hasChanged) {
          // File changed externally, must re-read
          const content = fs.readFileSync(resolvedPath, 'utf-8');
          this.fileStates.set(resolvedPath, {
            content,
            mtime: stats.mtimeMs,
            size: stats.size,
            inode: stats.ino
          });
          return { content, source: 'disk', changed: true };
        }
        
        // File truly unchanged
        return { 
          suppressed: true, 
          message: "File unchanged since your last Read. Refer to that earlier tool_result instead." 
        };
      }
      
      // First read
      const content = fs.readFileSync(resolvedPath, 'utf-8');
      this.fileStates.set(resolvedPath, {
        content,
        mtime: stats.mtimeMs,
        size: stats.size,
        inode: stats.ino
      });
      return { content, source: 'disk', changed: false };
      
    } catch (err) {
      // File doesn't exist or can't be read
      this.fileStates.delete(resolvedPath);
      throw err;
    }
  }
}

Fix 2: Invalidate Cache on External Watch Events

For VSCode/IDE integration, listen to file change events:

// src/integrations/vscode/fileWatcher.ts
const setupFileWatcher = (context: vscode.ExtensionContext, readTool: ReadTool) => {
  const watcher = vscode.workspace.createFileSystemWatcher('**/*');
  
  watcher.onDidChange(uri => {
    const filePath = uri.fsPath;
    // Invalidate the read cache for this file
    readTool.invalidate(filePath);
  });
  
  watcher.onDidCreate(uri => {
    readTool.invalidate(uri.fsPath);
  });
  
  watcher.onDidDelete(uri => {
    readTool.invalidate(uri.fsPath);
  });
};

Fix 3: Add Staleness Indicator

When returning suppressed results, include staleness info:

return { 
  suppressed: true, 
  message: "File unchanged since your last Read (last read: 2m ago). Refer to that earlier tool_result instead.",
  lastReadTimestamp: cached.timestamp,
  diskMtime: stats.mtimeMs,
  isStale: stats.mtimeMs !== cached.mtime
};

Testing Plan

  1. Read a file (Read #1)
  2. Modify the file externally (via terminal, IDE, or script)
  3. Read the same file again (Read #2)
  4. Verify: Read #2 returns current content, not stale cached version
  5. Verify: no "file unchanged" message when file has actually changed

Risk Assessment

  • Low risk: The fix adds disk-state verification
  • Performance impact: Minimal (stat call is fast)
  • No breaking changes: Existing behavior preserved when file truly unchanged

Estimated Effort

  • Core fix (disk-state check): 2-3 hours
  • VSCode file watcher: 1-2 hours
  • Testing: 1 hour
  • Total: ~4-6 hours

---
Solution submitted for review. Estimated effort and quote available upon request.

chadj2 · 4 months ago
1. [BUG] Read tool returns "Unchanged since last read" after git checkout changes file content #45748

Apparently Jupyter notebooks have a specialized harness to manipulate cells within it. #45748 is a similar issue related to reading a text file after a git operation; however, this bug affects the read operations of specific cells using the Jupyter notebook harness. Perhaps there is common code that affects read operations in both cases but that would need to be confirmed.

github-actions[bot] · 3 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

rstander7777 · 3 months ago

This is still happening in v2.1.185 today. It is a GENERAL bug that needs to be fixed. It should not have a status of NOT PLANNED.

If I tell the LLM that I modified the file the following happens:

  1. LLM mods file
  2. User mods file and deletes start lines using external editor and SAVES
  3. User tells LLM that they modded the file and to read it again.
  4. LLM uses read tool. Read tool says nothing was modified.
  5. LLM uses read tool again for changes at the end. Read tool says: Read 0 lines
  6. LLM uses read tool again but to get the WHOLE file. Read tool says: Read 33 lines

Here is the output from Claude CLI:
I modified tasks.txt. Add another task at the end for the following: Verify that Content\Scripts\Examples\12-zoom-spiral-feedback.js is producing the output as expected. It only draws a filled circle in the middle of the screen and nothing else. Please fix.
● Read(F:\Programming\CSharp\MusicTo3D\docs\tasks.txt)
Unchanged since last read
● Read(F:\Programming\CSharp\MusicTo3D\docs\tasks.txt · lines 40-79)
Read 0 lines
● Read(F:\Programming\CSharp\MusicTo3D\docs\tasks.txt)
Read 33 lines

chadj2 · 3 months ago

This should be a simple fix. We even have a code proposal. All these AI tools and its too much work for Claude to fix itself?

chadj2 · 3 months ago
This is still happening in v2.1.185 today. It is a GENERAL bug that needs to be fixed. It should not have a status of NOT PLANNED. If I tell the LLM that I modified the file the following happens: 1. LLM mods file 2. User mods file and deletes start lines using external editor and SAVES 3. User tells LLM that they modded the file and to read it again. 4. LLM uses read tool. Read tool says nothing was modified. 5. LLM uses read tool again for changes at the end. Read tool says: Read 0 lines 6. LLM uses read tool again but to get the WHOLE file. Read tool says: Read 33 lines

@rstander7777 I observed this in the notebook (ipynb) file extension. I don't see this with regular text files.

github-actions[bot] · 1 month ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.