[BUG] Terminal cursor position drift / scrollback corruption on Windows (PowerShell, VS Code)

Resolved 💬 3 comments Opened Dec 16, 2025 by Fuijin Closed Dec 20, 2025

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?

Description

Terminal cursor position drifts horizontally during output streaming, causing text to render at incorrect positions. This creates a "scrollback corruption" effect where previous output becomes misaligned.

Environment

  • Claude Code Version: 2.0.70 (native installer)
  • OS: Windows 11
  • Terminal: VS Code integrated terminal (PowerShell)
  • Node: v22.17.0

Reproduction

  1. Open Claude Code in VS Code integrated terminal (PowerShell)
  2. Send a prompt that generates multi-line output
  3. Observe horizontal text drift during streaming
  4. Scroll back - previous content is misaligned

Screenshot

<!-- Add screenshot here showing the cursor drift -->

Workaround

Until fixed, users can wrap Claude Code with the above fix. Full wrapper script:

<details>
<summary>claude-wrapper.js (click to expand)</summary>

#!/usr/bin/env node
/**
 * Claude Code Wrapper - Fixes ANSI cursor position bug
 * GitHub Issues: #9935, #8618, #10772
 */

const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');

// ANSI escape sequences
const SAVE_CURSOR = '\x1b[s';
const RESTORE_CURSOR = '\x1b[u';
const CLEAR_LINE = '\x1b[2K';
const CURSOR_HOME = '\x1b[G';
const HIDE_CURSOR = '\x1b[?25l';
const SHOW_CURSOR = '\x1b[?25h';

// Batch writes at 16ms (60 FPS)
const FLUSH_INTERVAL_MS = 16;
let outputBuffer = '';
let lastFlush = Date.now();
let flushTimer = null;

function flushOutput() {
    if (outputBuffer.length > 0) {
        process.stdout.write(outputBuffer);
        outputBuffer = '';
        lastFlush = Date.now();
    }
    flushTimer = null;
}

function batchWrite(str) {
    outputBuffer += str;
    const now = Date.now();
    if (now - lastFlush >= FLUSH_INTERVAL_MS) {
        flushOutput();
    } else if (!flushTimer) {
        flushTimer = setTimeout(flushOutput, FLUSH_INTERVAL_MS - (now - lastFlush));
    }
}

let inLineUpdate = false;

function fixAnsiOutput(chunk) {
    let str = chunk.toString();
    const hasCarriageReturn = str.includes('\r') && !str.includes('\r\n');
    const hasNewline = str.includes('\n');

    if (hasCarriageReturn) {
        if (!inLineUpdate) {
            str = HIDE_CURSOR + SAVE_CURSOR + CURSOR_HOME + CLEAR_LINE + str;
            inLineUpdate = true;
        } else {
            str = CURSOR_HOME + CLEAR_LINE + str;
        }
    }

    if (hasNewline && inLineUpdate) {
        str = RESTORE_CURSOR + SHOW_CURSOR + '\n' + str.replace(/^\r?\n/, '');
        inLineUpdate = false;
    }

    return str;
}

// Find claude executable
function findClaude() {
    const locations = [
        path.join(process.env.USERPROFILE || '', '.local', 'bin', 'claude.exe'),
        path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
        'claude'
    ];
    for (const loc of locations) {
        try { fs.accessSync(loc); return loc; } catch {}
    }
    return 'claude';
}

const child = spawn(findClaude(), process.argv.slice(2), {
    stdio: ['inherit', 'pipe', 'pipe'],
    shell: process.platform === 'win32'
});

child.stdout.on('data', (chunk) => batchWrite(fixAnsiOutput(chunk)));
child.stderr.on('data', (chunk) => process.stderr.write(chunk));
child.on('exit', (code) => { flushOutput(); process.exit(code || 0); });

</details>

Labels

bug, area:tui, platform:windows, has repro

What Should Happen?

Root Cause Analysis

Based on investigation of the minified cli.js and related GitHub issues (#9935, #8618, #10772):

  1. Full-screen redraws on every stream chunk instead of incremental updates
  2. 4,000-6,700 scroll events/second measured (from #9935)
  3. Missing cursor save/restore - found 24x showCursor but 0x saveCursor/restoreCursor
  4. No line-clearing before carriage return updates

Error Messages/Logs

Steps to Reproduce

Proposed Fix

We developed a working wrapper that fixes the issue:

// ANSI escape sequences
const SAVE_CURSOR = '\x1b[s';
const RESTORE_CURSOR = '\x1b[u';
const CLEAR_LINE = '\x1b[2K';
const CURSOR_HOME = '\x1b[G';
const HIDE_CURSOR = '\x1b[?25l';
const SHOW_CURSOR = '\x1b[?25h';

// Batch writes at 16ms (60 FPS) - reduces scroll events from ~5000/sec to ~60/sec
const FLUSH_INTERVAL_MS = 16;
let outputBuffer = '';
let lastFlush = Date.now();

function batchWrite(str) {
    outputBuffer += str;
    const now = Date.now();
    if (now - lastFlush >= FLUSH_INTERVAL_MS) {
        process.stdout.write(outputBuffer);
        outputBuffer = '';
        lastFlush = now;
    }
}

// Cursor state tracking
let inLineUpdate = false;

function fixAnsiOutput(chunk) {
    let str = chunk.toString();
    const hasCarriageReturn = str.includes('\r') && !str.includes('\r\n');
    const hasNewline = str.includes('\n');

    if (hasCarriageReturn) {
        if (!inLineUpdate) {
            // First line update - hide cursor, save position, clear line
            str = HIDE_CURSOR + SAVE_CURSOR + CURSOR_HOME + CLEAR_LINE + str;
            inLineUpdate = true;
        } else {
            // Subsequent updates - just clear and rewrite from home
            str = CURSOR_HOME + CLEAR_LINE + str;
        }
    }

    if (hasNewline && inLineUpdate) {
        // Line update finished - restore cursor, show cursor
        str = RESTORE_CURSOR + SHOW_CURSOR + '\n' + str.replace(/^\r?\n/, '');
        inLineUpdate = false;
    }

    return str;
}

Key Changes Needed in Claude Code

  1. Add SAVE_CURSOR / RESTORE_CURSOR around spinner/status updates
  2. Add CLEAR_LINE + CURSOR_HOME before overwriting lines
  3. Batch terminal writes at 16ms intervals (60 FPS cap)
  4. Hide cursor during rapid updates to prevent flicker

Related Issues

  • #9935 - Excessive scroll events (4,000-6,700/sec)
  • #8618 - Terminal UI Rendering Corrupted
  • #10772 - Terminal Flickering (v2.0.29+)
  • #674 - Cursor Style Interference

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.0.70

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

VS Code integrated terminal

Additional Information

_No response_

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗