When The Terminal Spoke Devanagari

Two hours into an edit-heavy Claude Code session, the terminal pane stopped rendering Latin script and started showing what looked like Devanagari. The file was intact. The renderer had lost its cascade. The trust cost of a font bug that looks like data loss is higher than the bug itself.

The cafe in Prรญncipe Real had decent espresso and slow wifi, which is the right tradeoff if you came to write code instead of watch dashboards. Two hours into a deck design session with Claude Code in the VS Code integrated terminal โ€” many small Edits, many Reads, the M1 fan staying polite โ€” the terminal pane committed an act of light vandalism. Mid-tool-call, every character of Claude's output turned into Devanagari.

Not garbage Unicode. Not mojibake. Actual recognizable script, in the actual structural position where Latin letters should have been. The ANSI color codes were intact โ€” red/green diff highlighting still applied. The session chrome at the bottom ("TERMINAL", session title, token meter) was still in English. Just the assistant's words and the tool-result text were now a foreign alphabet.

The first instinct was panic. The HTML file Claude had been editing for two hours might be ruined. Hours of work might be hours of garbage.

The first instinct was wrong.


file Says UTF-8, Eyes Say Devanagari

The diagnostic took thirty seconds. The bytes told a completely different story than the screen.

$ file "Lovro Pitch Deck.html"
Lovro Pitch Deck.html: HTML document text, Unicode text, UTF-8 text

$ grep -c '<\|>' "Lovro Pitch Deck.html"
0

$ wc -c "Lovro Pitch Deck.html"
   83472 Lovro Pitch Deck.html

Three checks. UTF-8 confirmed. No double-encoded entities. Byte count consistent with the work I remembered doing. The file was untouched.

Opening the deck in Chrome confirmed it from the rendering side: every glyph correct, every layout intact, every named entity resolving to its proper character. The work was fine.

The thing that was broken was the terminal renderer in VS Code. The bytes were leaving Claude's tool output as ordinary UTF-8. Something inside xterm.js (or the macOS font subsystem feeding it) had decided that the right font to render those bytes was not the configured monospace face. It had picked a fallback. And once it picked the fallback, it kept picking it.


The Font Fallback Cascade

Terminal emulators do not own glyphs. They own a cascade. When a character comes through and the active font does not contain a glyph for it, the renderer walks a chain of installed system fonts, finds one that does, and uses it. Most of the time, this cascade is invisible โ€” you hit an emoji, the emoji font kicks in for that one character, the next character returns to your monospace face.

Most of the time.

The failure mode looks like this. A single character flows through that the active font lacks. The fallback font that satisfies it happens to also claim coverage for a much broader range โ€” Latin-A, Latin extended, basic punctuation. The renderer caches that fallback for performance. The cache, in a bad state, doesn't release. Now every subsequent Latin character is being satisfied by a font that does contain those glyphs but in a completely different script's design โ€” because the fallback face was, say, a Devanagari Sans built with Latin coverage as a side feature.

Healthy cascade:
  char 'A' -> Menlo (hit) -> render 'A'
  char 'โ‚ฌ' -> Menlo (miss) -> Symbol Fallback (hit) -> render 'โ‚ฌ'
  char 'B' -> Menlo (hit) -> render 'B'

Broken cascade (sticky fallback):
  char 'A' -> Menlo (hit) -> render 'A'
  char 'โŸฆ' -> Menlo (miss) -> Devanagari Sans (hit) -> render 'โŸฆ'
  char 'B' -> Devanagari Sans (still active in cache) -> renders as 'เคฌ'
  char 'C' -> Devanagari Sans -> renders as something else
  ...

The renderer is not malfunctioning at the byte level. It is malfunctioning at the font selection level. The bytes hitting the screen are the right bytes. The face being used to draw them is wrong.

What likely triggered it in this session was a combination โ€” many HTML named entities (“, ’, —), box-drawing dividers (โ•โ•), a data-label="?" attribute, and heavy ANSI-colored diff output. None of these characters are themselves exotic. But the combination, at high throughput, on a renderer whose fallback logic isn't idempotent, was enough.


What A Real Diagnostic Would Have Said

The user-facing failure was misnamed. The terminal showed gibberish where Claude's response should have been. The natural inference, especially for someone not deep in font-rendering internals, is that Claude broke the file. Because Claude was the last thing to touch the file. Because the gibberish appeared inside Claude's pane. Because the visual story is "the assistant emitted nonsense and your work is the nonsense."

A working diagnostic flow should have separated the two layers immediately:

HACK LOVE BETRAY
COMING SOON

HACK LOVE BETRAY

Mobile-first arcade trench run through leverage, trace burn, and betrayal. The City moves first. You keep up or you get swallowed.

VIEW GAME FILE โ†’
[ASSISTANT DETECTION]
  symptom: rendered output illegible
  probable_cause: terminal font fallback corruption (renderer-side)
  not: file corruption
  not: assistant output corruption
  verify_now:
    1. file <path>          # confirms encoding
    2. grep entities <path> # confirms no double-encoding
    3. open in browser      # confirms render
  resolve:
    - close and reopen integrated terminal pane
    - if persists, restart VS Code
    - the file is intact

The diagnostic above is roughly the conversation that should have happened automatically, the moment the user typed "the terminal turned to Thai." Three commands. One sentence of reassurance. Total time to "your work is safe": under a minute.

Instead, the default conversation in this kind of failure is human-paced reasoning toward the right answer โ€” "let me check if the file is okay," "let me look at git status," "let me open it in another tool" โ€” which works, but burns trust on the way to its conclusion. The user, in the meantime, is staring at a screen that has the shape of catastrophic data loss.


The Trust Cost Of A Rendering Bug

This is what makes a rendering bug worse than its severity.

If Claude Code emits a wrong character, the bug is in the output and the user reads it as a bug in the output. If Claude Code's renderer emits the right character in a wrong font, the bug is in the chrome and the user reads it as a bug in the work. Those two readings have very different consequences. The first costs a re-prompt. The second costs the user's confidence that the tool is reliable around their data.

The asymmetry matters because the assistant is the surface. The terminal pane is the only window into what the assistant did. When the window goes dark โ€” or in this case, switches alphabets โ€” there is no second window the user can cross-check against without leaving the tool entirely.

The fix is split across two layers and neither is hard.

Terminal-side (xterm.js / VS Code): Make the font fallback cascade idempotent. A single character lookup must not permanently swap the active font for subsequent lookups. The fix is small. The bug is old. It deserves attention from the team because Claude Code's primary surface inherits the bug whenever it runs in this terminal.

Assistant-side (Claude Code): When the user describes a symptom that pattern-matches to "terminal rendering failure" โ€” "turned to Thai," "garbage characters," "can't read your output" โ€” the assistant should immediately run the integrity check above and surface the renderer-vs-data distinction before engaging with whatever the user thinks went wrong. The diagnostic is cheap. The reassurance is the product.

Both-sides: A subtle indicator when the renderer's font fallback chain changes โ€” a one-character status flag, a color cue in the prompt โ€” would let the user identify the renderer as the suspect without diagnostic conversation at all.


The Workaround, And The Bigger Note

The workaround is short:

1. Close the integrated terminal pane (the trash icon, not the X).
2. Reopen a new integrated terminal in the same workspace.
3. The session content is preserved by Claude Code's session model.
4. The file you were editing was never affected.

But the bigger note is the one worth ending on. Tools whose primary surface is the terminal inherit the terminal's failure modes. When the terminal glitches, the tool appears to glitch. The assistant has an opportunity โ€” and arguably an obligation โ€” to recognize when the user is staring at a renderer bug and to say so out loud before the user reaches for the undo key.

The file was fine. The deck rendered in Chrome the whole time. The two hours of work were never in danger. The only thing in danger was the user's belief that the tool could be trusted around their work โ€” and that's a fix for the product team, not a fix for the user.

Filed under "rendering bugs that look like data bugs." A category that deserves its own diagnostic flow.


GhostInThePrompt.com // The bytes were right. The font was wrong. The fix is in the cascade, the diagnostic, and the reassurance โ€” in that order.