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:
