Markdown links in tables expand to full URL instead of showing short link text
Description
When Claude outputs markdown links like [text](url) inside table cells, the link text is not displayed as a short clickable hyperlink. Instead, the full URL is shown (or the table falls back to card layout), losing the benefit of OSC 8 terminal hyperlinks.
Outside of tables, links render correctly as clickable text via OSC 8 escape sequences.
Steps to reproduce
Ask Claude to output a markdown table containing links, e.g.:
| Name | Link |
|------|------|
| Example | [click here](https://example.com/very/long/path/to/resource) |
| Docs | [documentation](https://docs.example.com/guide/getting-started) |
Expected: Table cells show short clickable text ("click here", "documentation")
Actual: The full URL is displayed, or the table switches to card/list layout because the OSC 8 escape sequences inflate the perceived column width
Root cause analysis
After investigating the minified source (cli.js from the npm package), here's what happens:
The rendering pipeline
marked.lexer()parses markdown into tokens- Tables are extracted and rendered via a dedicated Ink component (
LY4in minified code) - Other tokens go through
MX(), a custom token-to-ANSI renderer - Links are wrapped in OSC 8 via
_Z6():\x1B]8;;URL\x07display_text\x1B]8;;\x07
The link renderer works correctly
The "link" case in MX() correctly produces OSC 8 hyperlinks:
case "link": {
let text = (token.tokens ?? []).map(t => MX(t, ...)).join("");
let plainText = stripAnsi(text);
if (plainText && plainText !== token.href)
return makeHyperlink(token.href, text); // [text](url) → OSC 8 with display text
return makeHyperlink(token.href); // bare URL
}
The bug is in table width calculation
In the LY4 table component, column widths are calculated by:
- Rendering cell content via
MX()(which produces OSC 8 sequences for links) - Stripping ANSI with
q_()(strip-ansi) to get plain text - Measuring width with
f1()(string-width)
The problem: The strip-ansi function (q_) uses a regex (QA9) designed for standard CSI sequences (\x1B[...m), but OSC 8 sequences (\x1B]8;;...\x07) are not fully handled during width calculation. This causes:
- Column widths to be miscalculated (the URL inside the OSC 8 sequence inflates the perceived width)
- The table to exceed terminal width
- Fallback to card/list layout where link formatting is lost entirely
Additionally, the word-wrap function (E36, which is Bun.wrapAnsi or a JS fallback) may split OSC 8 escape sequences across lines when wrapping cell content, breaking the hyperlink.
Where in the code
In minified cli.js (v2.1.81):
_Z6— creates OSC 8 hyperlinks (TY4 = "\x1B]8;;",kY4 = "\x07")MX— token-to-ANSI renderer (handles all markdown token types)LY4— Ink table component with width calculationq_— strip-ansi function (uses regexQA9)f1— string-width functionyQ6→E36— word-wrap function
Suggested fix
In the table component's width-calculation helpers, ensure OSC 8 sequences are stripped before measuring:
// The strip function should also remove OSC 8 hyperlink sequences
// before measuring column widths:
function stripForWidth(text) {
return text
.replace(/\x1B]8;;[^\x07]*\x07([^\x1B]*)\x1B]8;;\x07/g, '$1') // keep display text only
.replace(ansiRegex, ''); // then strip remaining ANSI
}
This ensures [short text](very-long-url) is measured as the width of "short text", not the width of the entire escape sequence.
Environment
- Claude Code version: 2.1.81
- Terminal: iTerm2 / macOS (supports OSC 8)
- OS: macOS
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗