Terminal renderer miscalculates line width with ANSI escape codes, causing text corruption on Windows

Resolved 💬 19 comments Opened Dec 19, 2025 by DoctorFrankie Closed Mar 17, 2026

Bug Report: Terminal renderer miscalculates line width with ANSI escape codes, causing text corruption

Description

Inline code blocks (backticks) and other styled text render incorrectly in the terminal. Text appears shifted, truncated, or wrapped at wrong positions. This appears to be caused by ANSI escape codes being counted as visible characters during line width calculation.

Environment

  • Claude Code Version: 2.0.73
  • OS: Windows 10/11
  • Terminal: Windows Terminal (WT_SESSION present)
  • Shell: Git Bash (bash.exe)
  • TERM: xterm-256color

Steps to Reproduce

  1. Run Claude Code in Windows Terminal with Git Bash
  2. Ask Claude a question that results in a response containing inline code (backticks) with a long path or text
  3. Example prompt: "Do you see the Schedule.xlsx file in the admin folder?"
  4. Claude responds with text like:

``
Yes, I can see **Schedule.xlsx** in the Admin folder at:
D:\Projects\Schedule\Admin\Schedule.xlsx
``

Expected Behavior

The path should render correctly on one or two lines with proper wrapping.

Actual Behavior

The text is corrupted:

  • Line 1 shows: "Yes, I can see Schedule.xlsx in the Admin folder at:"
  • Line 2 shows (with large left indent): "edulars\Admin\Schedule.xlsx"

The beginning of the path ("D:\Projects\Schedule") is missing or pushed off-screen. The continuation line has incorrect indentation.

Bullet lists following styled text also render incorrectly with items appearing fragmented or in wrong positions.

Root Cause Analysis

The terminal renderer appears to count ANSI escape codes (used for inline code styling) as visible characters when calculating line width for wrapping. For example:

  • Escape codes like \x1b[48;5;240m (background color) add ~12 invisible bytes
  • These are counted in the string length calculation
  • This shifts the calculated cursor position
  • When text wraps, the continuation appears at incorrect column positions

Evidence

  • Issue only affects styled content (inline code, bold, etc.)
  • Plain text without markdown formatting renders correctly
  • The offset appears proportional to the number of styling codes applied

Workaround

Setting NO_COLOR=1 environment variable before launching Claude Code may disable ANSI styling and avoid the issue (not fully tested).

Additional Context

This issue has been reported by multiple users on Twitter, suggesting it's a widespread problem affecting Windows Terminal users.

View original on GitHub ↗

19 Comments

weto4ka · 6 months ago

Have same issue since today. use windows as well, and Jet Brain Rider ide together with Claude code.

cjteams · 6 months ago

same issue here

Burtrola · 6 months ago

Same. Just started happening when I started a new session. Using CC in VS code and tested in Windows CMD just to rule out VS Code. Same results. Missing/partial sentences, lots of extra random whitespaces/lines and line breaks mid-word.

gigios · 6 months ago

I have the same problem with the Claude Code client for Windows!
It's really hard to understand what's written!

Skeletoneyes · 6 months ago

Same started happening to me today. I find it's more often happening in Powershell than CMD

TNYCL · 6 months ago

same, in windows git bash and macos bash same.

DanSandoval · 6 months ago

same issue. on v2.0.76

slshults · 6 months ago

+1

spiveym · 6 months ago

This issue persists in the latest version.

Environment:

  • Claude Code: 2.1.6
  • OS: Windows 10 (Build 19045)
  • Terminal: Windows Terminal
  • Shell: Git Bash 5.2.26 (MINGW64)
  • Node: v20.11.0

Observations:

  • Colored text fragments appear scattered across output lines in wrong positions
  • Not related to custom statusLine, as the issue persists with "disableAllHooks": true in settings
  • Affects normal Claude output, not just code blocks

Screenshot:

<img width="1662" height="141" alt="Image" src="https://github.com/user-attachments/assets/54d59a8a-408a-4639-8772-8c2322e17116" />

<img width="718" height="179" alt="Image" src="https://github.com/user-attachments/assets/2b58a7f0-5eaf-457e-bb38-008dbfe00fb8" />

<img width="899" height="464" alt="Image" src="https://github.com/user-attachments/assets/31d89a14-b463-4480-9a2b-63880b9d45d0" />

netinput · 5 months ago

This was bothering me too for a while. Here's a dirty patch for postinstall:

Update: can I just update the patch because like why not and it can be useful for someone?

import fs from "fs";
import { fileURLToPath } from "url";
import path from "path";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const file = path.join(__dirname, "claude-code", "cli.js");

let content = fs.readFileSync(file, "utf8");

const NORMALIZER_SIG = `A=>A.normalize().replaceAll("\\r\\n","\\n")`;
const N_DEF = `let NORM=A=>A.normalize().replaceAll("\\r\\n","\\n"),`;

if (content.includes(NORMALIZER_SIG)) {
  console.log("Already patched");
  process.exit(0);
}

const re = /}\s*else\s*if\s*\(\s*(?![A-Za-z_$][\w$]*\s*===\s*["']wrap-trim["'])([A-Za-z_$][\w$]*)\s*\)\s*\{\s*let\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*,\s*([A-Za-z_$][\w$]*)\s*,\s*([A-Za-z_$][\w$]*)\s*\)\s*,\s*([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*\)\s*;\s*([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\(\s*\2\s*,\s*\9\s*,\s*\7\s*,\s*\4\s*,\s*\6\s*===\s*["']wrap-trim["']\s*\)\s*}/s;

function mapNormalize(arrIdent) {
  return `${arrIdent}.map(_=>({..._,text:NORM(_.text)}))`;
}

let changed = false;

const out = content.replace(re, (m, condIdent, strVar, strFn, strArg, midArg, modeVar, arrVar, arrFn, arrArg, outVar, outFn) => {
  changed = true;
  const arrMapped = mapNormalize(arrArg);

  return `}else if(${condIdent}){${N_DEF}${strVar}=${strFn}(${strArg},${midArg},${modeVar}),${arrVar}=${arrFn}(${arrMapped});${outVar}=${outFn}(${strVar},${arrArg},${arrVar},NORM(${strArg}),${modeVar}==="wrap-trim")}`;
});

if (!changed) {
  console.log('Pattern not found - different version?');
  process.exit(1);
}

fs.writeFileSync(file, out, "utf8");
console.log("Patched successfully!");

Color Shift Bug Fix

Root Cause

In cli.js line 599, the yv3 function applies styles to wrapped text using a style map created by xv3. The bug occurs because:

  1. Text wrapping (f_/Fl function) normalizes text: .normalize().replaceAll("\r\n", "\n")
  2. Style map (xv3) was created from unnormalized style object texts
  3. Position tracking in yv3 uses the normalized text length
  4. Length mismatch: If style objects contain unnormalized Unicode (e.g., decomposed characters), the style map has different length than the normalized text
  5. Result: Position tracker drifts, causing styles to apply at wrong positions

Why all 4 conditions are required

  • Bold + Code: Creates multiple style objects (the bug path requires F.length > 1)
  • Quotes: Likely triggers text with characters that normalize differently
  • Wrap: The yv3 multi-line code path is only executed when text wraps

The Fix

Normalize style object texts before creating the style map:

What the fix does

  1. Creates helper N to normalize text: A=>A.normalize().replaceAll("\r\n","\n")
  2. Normalizes each style object's text before xv3: F.map(_=>({..._,text:N(_.text)}))
  3. Normalizes the original text passed to yv3: N(H)
xrf-9527 · 5 months ago

<img width="377" height="355" alt="Image" src="https://github.com/user-attachments/assets/52d8c6ba-afca-4942-af8b-6ffd54e087cb" />

and what‘s this?

version:claude code 2.1.5
os:macOS

frostbtn · 5 months ago
Update: can I just update the patch because like why not and it can be useful for someone? `` import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; ... ``

Unfortunately, this won't work with "native" installation -- as it's a single massive .exe digitally signed.
And as of v2.1.19 they seem to handle this differently, so they've made changes there but did not fix the issue.

But I liked @babanga 's way of thinking. So, for anyone terminally desperate, here's a gist. It's an old-school runtime patcher to fix the vibe-coded slop: no you don't need to remove \r from every \r\n just because it is Windows! At least I restored my sanity, at least in Windows 10 - Windows Terminal - PowerShell - GitBash -- markdown formatting is properly aligned once again. But you need to be really desperate to use it, as runtime patcher is an overkill.

WargameGPT · 5 months ago

Root Cause Found & One-Line Fix

The bug is in the IP5 function in cli.js (the Ink TUI text renderer). This function applies ANSI styles to word-wrapped text by walking through both the wrapped output and the original text in parallel, using a position tracker $ and a style map K[$].

The problem: When word-wrapping breaks a line at a space, that space is consumed (removed from the wrapped output). But $ only advances once per character of the wrapped text. After each line break, $ is behind by 1 (the consumed space). This accumulates: after N line wraps, styles are shifted N characters to the right.

The fix already exists in the code — it's just gated behind trim mode. The IP5 function has a whitespace-position-correction block that detects when the original text has whitespace at $ but the wrapped line doesn't start with whitespace, and skips past the consumed space. But this logic is behind if(z&&_.length>0) where z is only true in "wrap-trim" mode. The default "wrap" mode never runs it.

The Fix

In the IP5 function, change:

if(z&&_.length>0){let W=/\s/.test(_[0]);if($<Y.length&&/\s/.test(Y[$])&&!W)while($<Y.length&&/\s/.test(Y[$]))$++}

to:

if(_.length>0){let W=/\s/.test(_[0]);if($<Y.length&&/\s/.test(Y[$])&&!W)while($<Y.length&&/\s/.test(Y[$]))$++}

That's it. Remove z&& from the condition. The whitespace correction now runs in all wrapping modes, not just trim.

Why This Explains All Observed Behavior

  • Code blocks unaffected: Code blocks are single-style (M.length===1), so they go through a different branch that applies Ty1 to whole lines — no position tracking needed.
  • Prose with formatting drifts: Mixed formatting (bold + normal + code + italic) produces multiple style objects (M.length > 1), hitting the IP5 path where the bug lives.
  • Drift accumulates: Each word-wrap boundary that consumes a space adds +1 character of rightward style shift.
  • Longer paragraphs = worse drift: More line wraps = more consumed spaces = more accumulated offset.

How to Apply

Search cli.js for z&&_.length>0 — there's exactly one match. Remove the z&&. Tested on 2.1.33, completely eliminates the prose highlighting drift.

Note: if you ask Claude Code to fix itself using this information, it can locate and patch the function automatically.

WargameGPT · 5 months ago

Update: Second patch needed for plan mode / indented content

The original fix (removing z&& from the start-of-line whitespace correction) resolves drift for regular prose, but plan mode content with indented markdown (bullet lists, numbered steps, sub-items) still drifts.

Why

The start-of-line whitespace skip has a condition !W that checks whether the current wrapped line starts with non-whitespace. For indented continuation lines (common in plan mode markdown), the line DOES start with whitespace, so W is true and !W is false — the correction is skipped entirely.

There's a second whitespace correction block at the end of the line loop in IP5 that uses look-ahead matching instead. It finds the first character of the next wrapped line and advances $ until it aligns. This handles indented content correctly. But it was also gated behind z&& (trim-mode only).

Complete fix (two changes in IP5)

Change 1 (start-of-line, handles non-indented wraps):

// Before:
if(z&&_.length>0){let W=/\s/.test(_[0]);if($<Y.length&&/\s/.test(Y[$])&&!W)while($<Y.length&&/\s/.test(Y[$]))$++}
// After:
if(_.length>0){let W=/\s/.test(_[0]);if($<Y.length&&/\s/.test(Y[$])&&!W)while($<Y.length&&/\s/.test(Y[$]))$++}

Change 2 (end-of-line, handles indented/plan content wraps):

// Before:
if(z&&O<w.length-1){let W=w[O+1],G=W.length>0?W[0]:null;while($<Y.length&&/\s/.test(Y[$])){if(G!==null&&Y[$]===G)break;$++}}
// After:
if(O<w.length-1){let W=w[O+1],G=W.length>0?W[0]:null;while($<Y.length&&/\s/.test(Y[$])){if(G!==null&&Y[$]===G)break;$++}}

Both changes are the same: remove z&& so the whitespace correction runs in all wrap modes, not just "wrap-trim".

How to apply

Search cli.js for z&& — within the IP5 function there are exactly two occurrences. Remove both. Confirmed working on 2.1.33 for both regular prose and plan mode with indented markdown content.

vargavik · 5 months ago

This PowerShell script patches the Claude exe on Windows binary installation. Thanks to all of you who investigated and found the cause. Needs to be executed after auto-update too.

# Fix for ANSI highlight drift (https://github.com/anthropics/claude-code/issues/17314)
# Pattern 1 needs "/\s/" context check to avoid matching unrelated code.
# Pattern 2 was already precise (only 2 matches in Bun double-embed).
# Expected: 4 total patches (2 patterns x 2 Bun embeds).

$binPath = Join-Path $env:USERPROFILE ".local\bin\claude.exe"
if (-not (Test-Path $binPath)) {
    Write-Host "Claude binary not found at $binPath"
    exit 1
}

Write-Host "Reading $binPath ..."
$bytes = [System.IO.File]::ReadAllBytes($binPath)
$enc = [System.Text.Encoding]::ASCII
$amp = [byte][char]'&'
$space = [byte][char]' '
$totalPatches = 0

# Pattern 1: X&&Y.length>0){let F=/\s/.test(Y[0])
# Anchor: ".length>0){let" + verify "/\s/" appears within next 10 bytes
$anchor1 = $enc.GetBytes('.length>0){let')
$verify1 = $enc.GetBytes('/\s/')

# Pattern 2: X&&Y<Z.length-1){let  (already precise — only IP5 has this)
$anchor2 = $enc.GetBytes('.length-1){let')

Write-Host "Scanning for pattern 1: .length>0){let ... /\s/ ..."
for ($i = 6; $i -lt $bytes.Length - $anchor1.Length - 15; $i++) {
    $match = $true
    for ($j = 0; $j -lt $anchor1.Length; $j++) {
        if ($bytes[$i + $j] -ne $anchor1[$j]) { $match = $false; break }
    }
    if (-not $match) { continue }

    # Check 4 pre-bytes: X&&Y where X is any char, && are ampersands, Y is any char
    if ($bytes[$i - 3] -ne $amp -or $bytes[$i - 2] -ne $amp -or $bytes[$i - 4] -eq $space) { continue }

    # Verify /\s/ appears within next 10 bytes after the anchor
    $hasRegex = $false
    $searchEnd = [Math]::Min($i + $anchor1.Length + 10, $bytes.Length - $verify1.Length)
    for ($k = $i + $anchor1.Length; $k -lt $searchEnd; $k++) {
        $vm = $true
        for ($v = 0; $v -lt $verify1.Length; $v++) {
            if ($bytes[$k + $v] -ne $verify1[$v]) { $vm = $false; break }
        }
        if ($vm) { $hasRegex = $true; break }
    }
    if (-not $hasRegex) { continue }

    $start = [Math]::Max(0, $i - 4)
    $end = [Math]::Min($bytes.Length - 1, $i + $anchor1.Length + 20)
    $ctx = $enc.GetString($bytes[$start..$end])
    Write-Host "  [Pattern 1] offset $($i-4): $ctx"
    $bytes[$i - 4] = $space
    $bytes[$i - 3] = $space
    $bytes[$i - 2] = $space
    $totalPatches++
}

Write-Host "Scanning for pattern 2: X&&Y<Z.length-1){let ..."
for ($i = 8; $i -lt $bytes.Length - $anchor2.Length; $i++) {
    $match = $true
    for ($j = 0; $j -lt $anchor2.Length; $j++) {
        if ($bytes[$i + $j] -ne $anchor2[$j]) { $match = $false; break }
    }
    if (-not $match) { continue }

    # Check 6 pre-bytes: X&&Y<Z
    if ($bytes[$i - 5] -ne $amp -or $bytes[$i - 4] -ne $amp -or $bytes[$i - 6] -eq $space) { continue }

    $start = [Math]::Max(0, $i - 6)
    $end = [Math]::Min($bytes.Length - 1, $i + $anchor2.Length + 15)
    $ctx = $enc.GetString($bytes[$start..$end])
    Write-Host "  [Pattern 2] offset $($i-6): $ctx"
    $bytes[$i - 6] = $space
    $bytes[$i - 5] = $space
    $bytes[$i - 4] = $space
    $totalPatches++
}

Write-Host "`nTotal patches applied: $totalPatches (expected: 4)"
if ($totalPatches -eq 4) {
    [System.IO.File]::WriteAllBytes($binPath, $bytes)
    Write-Host "Done. Restart Claude Code for the fix to take effect."
} elseif ($totalPatches -gt 0 -and $totalPatches -ne 4) {
    Write-Host "WARNING: Expected 4 patches but found $totalPatches. NOT writing to disk. Review the matches above."
} else {
    Write-Host "No patterns found. Binary may already be patched or the version has changed."
}
sebastian-zitzelsberger · 5 months ago

@claude Could somebody please pay attention to this issue? It is a know reproducable bug for months with considerable impact on the usabiltiy of your product, for everyone using it on windows. it also has probable root-cause and a fix associated with it.

rfmsanz-ethan · 4 months ago

This hook worked for me. I am using CC native ARM64 binary. I know the pattern is the same in the other architectures' binaries, but I don't know if there are any differences to take into account. You could start with these and ask Claude to check them and implement them according to your current installation.

OneDrive Folder with Hook

If anyone can get the attention of anthropic devs to implement a fix of the root cause (as identified by @WargameGPT above) that would be super.

@ant-kurt, @bcherny, @ashwin-ant, @fvolcic

github-actions[bot] · 4 months ago

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

github-actions[bot] · 3 months 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.