Skip to content

[BUG] Edit tool silently no-ops on disk while Read returns the edited content (sub-agent worktrees + main session) #61471

Closed as not planned

Description

@falaky

Preflight Checklist

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

What's Wrong?

Description:
The Edit tool returns success but the change is never written to disk. The Read tool subsequently returns the imagined post-edit content (an in-memory or cached view), but git status / git diff / awk / grep against the file all show the pre-edit content. The discrepancy is silent — no error, no warning.

What Should Happen?

Expected: Edit either persists the change OR errors loudly. Read should reflect on-disk state, not a hypothetical post-edit state.

Actual: Edit silently no-ops; Read returns stale (post-edit-in-memory) content; subsequent Bash commands see the unmodified file.

Frequency: ~8 incidents in one day across multiple sub-agent dispatches (~7 different isolation: "worktree" agents) plus once in the main session. Approx 30-50% hit rate on the first Edit of a session.

Expected: Edit either persists the change OR errors loudly. Read should reflect on-disk state, not a hypothetical post-edit state.

Actual: Edit silently no-ops; Read returns stale (post-edit-in-memory) content; subsequent Bash commands see the unmodified file.

Environment:

  • Claude Code CLI 2.1.148
  • Model: Opus 4.7 (1M context) — claude-opus-4-7[1m]
  • macOS Darwin 25.4.0
  • Multiple parallel sub-agents in isolation: "worktree" mode
  • Project: Next.js 16 + TypeScript + git worktrees

Notes:

  • The cached Read state survives across multiple Read calls in the same session — it's not a transient cache miss.
  • The pattern is most prevalent in nested worktrees (sub-agent dispatched from a sub-agent).
  • Suspected cause: the Edit tool's persistence layer may not be writing to the actual on-disk file when the agent's cwd is reported as one path but the harness has the file mounted at another (worktree mounting layer).

Error Messages/Logs

Steps to Reproduce

Steps to reproduce:

  1. Spawn a sub-agent with Agent({ isolation: "worktree", ... }) (also reproduces in the main session occasionally).
  2. Have the agent run Edit({ file_path: "/absolute/path/to/file.ts", old_string: "X", new_string: "Y" }).
  3. Tool returns "The file ... has been updated successfully."
  4. Have the agent run Read({ file_path: "/absolute/path/to/file.ts" }) — returns content with Y.
  5. Have the agent run Bash({ command: "git diff -- /absolute/path/to/file.ts" }) — diff is empty.
  6. Have the agent run Bash({ command: "awk 'NR==<line>' /absolute/path/to/file.ts" }) — prints the pre-edit X.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

No response

Claude Code Version

2.1.148

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

No response

Activity

  1. github-actions commented on May 22, 2026

    @github-actions

    Found 3 possible duplicate issues:

    1. [BUG] Sub-agents claim successful file creation but files don't persist to filesystem #4462
    2. [BUG] Claude Desktop 1.3109.0 (post-rewrite): Write/Edit tools silently fail to flush to disk mid-session — stale in-memory cache diverges from disk #51214
    3. Edit/Write tools silently fail to persist in deep isolated-worktree paths on Windows (stale-cache divergence) #61303

    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

  2. falaky commented on May 22, 2026

    @falaky
    Author

    Thanks for flagging the candidate duplicates. Reviewed each:

    Keeping this open — happy to be marked as related-but-not-duplicate, since the macOS-CLI repro adds a distinct datapoint.

  3. jshaofa-ui commented on May 22, 2026

    @jshaofa-ui

    claude-code #61471: Edit Tool Silently No-ops on Disk While Read Returns Edited Content — Fix

    Issue Summary

    The Edit tool reports success and subsequent Read calls return the edited content (from memory/buffer), but the actual file on disk is never modified. This is a silent data integrity failure — the user and agent believe the edit succeeded when it did not.

    Severity: Critical — silent data corruption, undermines trust in all edit operations
    Labels: bug, has repro, area:tools, area:core
    Competition: Zero

    Root Cause Analysis

    Primary Root Cause: Write-Ahead Buffer Without fsync Verification

    The Edit tool likely follows this flow:

    1. Read file content into memory buffer
    2. Apply string replacement in memory
    3. Write buffer to disk via fs.writeFile() or fs.promises.writeFile()
    4. Update internal cache/buffer with new content
    5. Return success to caller

    The failure occurs at step 3→4 boundary: the in-memory buffer is updated regardless of whether the disk write succeeded. Possible failure modes:

    1. Silent write failure: fs.writeFile() promise rejection is caught but not propagated — the catch block updates the buffer anyway
    2. File handle race: Another process (watcher, IDE, antivirus) holds a lock on the file, causing the write to fail silently on certain platforms
    3. Atomic write rollback: The tool uses a temp-file + rename pattern, but the rename fails (cross-device link, permissions) while the buffer is already updated
    4. VFS cache inconsistency: On some filesystems (network mounts, WSL, FUSE), the write succeeds at the syscall level but the VFS cache doesn't flush, and a subsequent read from a different file descriptor returns stale data

    Failure Pattern:

    Edit tool receives edit request
      → Load file into memory buffer
      → Apply replacement in buffer
      → Attempt disk write
        → WRITE FAILS (lock / permission / atomic rename error)
        → Error caught, logged at DEBUG level (invisible to user)
      → Buffer marked as "dirty → clean"
      → Return { success: true }
      → Subsequent Read returns buffer content (edited)
      → Disk still has old content
      → User/agent believes edit succeeded — data lost
    

    Why This Is Hard to Detect:

    • Read after Edit hits the in-memory cache, not disk
    • No verification step reads back from disk after write
    • Error logging is at DEBUG level or swallowed entirely
    • No checksum/hash comparison between buffer and disk

    Proposed Fix

    Fix 1: Post-Write Verification (Primary Fix)

    File: src/tools/edit-tool.ts (or equivalent)

    async function editFile(filePath: string, oldText: string, newText: string): Promise<EditResult> {
      // ... existing read + apply logic ...
      
      // Write to disk
      await fs.promises.writeFile(filePath, newContent, 'utf-8');
      
      // NEW: Verify write succeeded by reading back from disk
      const verifiedContent = await fs.promises.readFile(filePath, 'utf-8');
      if (verifiedContent !== newContent) {
        // Buffer and disk diverged — this is a critical failure
        throw new Error(
          `Edit verification failed for ${filePath}: disk content does not match expected output. ` +
          `Expected ${newContent.length} bytes, got ${verifiedContent.length} bytes. ` +
          `This may indicate a filesystem issue, file lock, or concurrent modification.`
        );
      }
      
      return { success: true, verified: true };
    }

    Fix 2: Atomic Write with Rollback

    import { tmpdir } from 'os';
    import { join } from 'path';
    
    async function editFileAtomic(filePath: string, oldText: string, newText: string): Promise<EditResult> {
      const newContent = applyEdit(existingContent, oldText, newText);
      
      // Write to temp file in same directory (same filesystem for atomic rename)
      const dir = path.dirname(filePath);
      const tempPath = join(dir, `.claude-edit-${process.pid}-${Date.now()}.tmp`);
      
      try {
        await fs.promises.writeFile(tempPath, newContent, 'utf-8');
        await fs.promises.rename(tempPath, filePath); // Atomic on same filesystem
      } catch (err) {
        // Clean up temp file on failure
        try { await fs.promises.unlink(tempPath); } catch {}
        throw err;
      }
      
      // Verify after rename
      const verified = await fs.promises.readFile(filePath, 'utf-8');
      if (verified !== newContent) {
        throw new Error(`Post-atomic-write verification failed for ${filePath}`);
      }
      
      return { success: true, verified: true };
    }

    Fix 3: Error Propagation (No Silent Swallowing)

    // Ensure errors are never swallowed
    async function executeEdit(params: EditParams): Promise<EditResult> {
      try {
        const result = await editFile(params.filePath, params.oldText, params.newText);
        return result;
      } catch (err) {
        // NEVER update buffer on failure
        // Propagate error with context
        return {
          success: false,
          error: `Edit failed for ${params.filePath}: ${err.message}`,
          // Include hint about possible causes
          hints: [
            'Check file permissions',
            'Check for file locks (IDE, watcher, antivirus)',
            'Check disk space',
            'Check if file is on a network mount'
          ]
        };
      }
    }

    Fix 4: Cache Invalidation on Write Failure

    // If write fails, invalidate the in-memory cache entry
    function invalidateCacheOnWriteFailure(filePath: string, err: Error): void {
      cacheManager.invalidate(filePath);
      // Force next Read to hit disk, not stale cache
      logger.warn(`Cache invalidated for ${filePath} after write failure: ${err.message}`);
    }

    Test Plan

    Unit Tests

    1. Mock write failure: Simulate fs.writeFile rejection → verify error propagated, buffer NOT updated
    2. Mock read-back mismatch: Simulate disk returning different content → verify verification error
    3. Concurrent modification: Simulate file changing between write and verify → verify detection
    4. Atomic write rename failure: Simulate cross-device rename error → verify fallback or clear error

    Integration Tests

    1. Read-only file: Attempt edit on read-only file → verify clear error (not silent noop)
    2. Locked file (Windows): Open file with exclusive lock, attempt edit → verify error
    3. Network mount: Edit file on NFS/SMB mount → verify write succeeds or fails clearly
    4. Large file: Edit large file (100MB+) → verify no partial writes

    Regression Tests

    1. Normal edit: Standard edit operation → verify success + disk match
    2. Multi-edit: Sequential edits to same file → verify all persisted
    3. Edit then Read: Edit file, then Read → verify content matches

    Risk Assessment

    Risk Likelihood Impact Mitigation
    Performance overhead from read-back verification Low Low Verification is fast for typical file sizes; add opt-out for very large files
    Verification fails on exotic filesystems Low Medium Add timeout and fallback; log warning but don't block
    Atomic write fails on cross-device paths Medium Low Detect cross-device, fall back to direct write with verification
    Breaking change for tools that rely on silent failures Very Low Low No known legitimate use case for silent failures

    Overall Risk: LOW — all changes are defensive, additive, and improve correctness without changing the happy path.

    References

  4. github-actions commented on Jun 22, 2026

    @github-actions

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

  5. github-actions commented on Aug 25, 2026

    @github-actions

    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.

  6. locked as resolved and limited conversation to collaborators on Aug 25, 2026
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

    area:agentsarea:toolsbugSomething isn't workinghas reproHas detailed reproduction stepsplatform:macosIssue specifically occurs on macOSstaleIssue is inactive

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions