[BUG] Terminal cursor position drift / scrollback corruption on Windows (PowerShell, VS Code)
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
- Open Claude Code in VS Code integrated terminal (PowerShell)
- Send a prompt that generates multi-line output
- Observe horizontal text drift during streaming
- 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):
- Full-screen redraws on every stream chunk instead of incremental updates
- 4,000-6,700 scroll events/second measured (from #9935)
- Missing cursor save/restore - found 24x
showCursorbut 0xsaveCursor/restoreCursor - 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
- Add
SAVE_CURSOR/RESTORE_CURSORaround spinner/status updates - Add
CLEAR_LINE+CURSOR_HOMEbefore overwriting lines - Batch terminal writes at 16ms intervals (60 FPS cap)
- 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_
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗