[BUG] Fenced code block inside a list item renders at column 0, losing the list indentation

Status Open
Reported on v2.1.268
Maintainer reply None cached
Activity 0 comments · opened Sep 11, 2026

Environment

  • Claude Code 2.1.268 (native install)
  • macOS (Darwin 27.0.0), TERM=xterm-256color

What happens

A fenced code block nested inside a list item is printed starting at column 0, so it
breaks out of the list it belongs to. The deeper the list, the more obvious the jump.

Source (typed into the prompt, and identical in assistant output):

  • a
  • b
  • c

``
FOO BAR
``

Rendered in the terminal:

  • a
  • b
  • c

FOO BAR

Expected: the fence body keeps the item's continuation indent (6 spaces here), so the
code block stays visually inside - c.

This is a follow-up to #82759, which fixed sibling indentation in nested lists by
introducing the listIndent option. listIndent is threaded through list and
list_item, but the code branch never consumes it, so fenced blocks were left behind.

Cause

In the markdown-to-terminal renderer (minified as lv(token, theme, opts) in the
bundle), applying the pad is the child's job, and the code branch never does it.

list_item hands block children the pad and returns their string untouched:

case "list_item": {
  const p = m + " ".repeat(len(marker) + 1);          // continuation indent
  const f = " ".repeat(Math.min(p.length, MAX));      // pad passed to children
  const L = (e.tokens ?? []).map((b) => {
    const y = lv(b, t, { ..., listIndent: f });
    if (b.type === "code" || b.type === "blockquote" || b.type === "hr"
        || b.type === "table" || b.type === "list" || b.type === "space")
      return y;                                       // <- returned as-is
    // only non-block children get re-indented:
    return y.split(E).map((I, _) => (_ === 0 ? S + I : I === "" ? I : p + I)).join(E);
  }).join("");
  ...
}

code ignores the pad it was given and emits its body verbatim:

case "code": {
  ...
  if (!s) return g + e.text + E;
  return g + s.highlight(e.text, { language: f }) + E;   // no per-line pad
}

listIndent appears only in the list and list_item branches, so nested lists keep
their column while fences do not. blockquote, hr and table take the same
untouched path, so they very likely lose their indent for the same reason.

Repro (extracted logic, plain node)

const NL = "\n";
const pad = (n) => " ".repeat(n);

function render(tok, opts = {}) {
  const { listDepth = 0, listIndent = "" } = opts;
  switch (tok.type) {
    case "list":
      return tok.items.map((it) => render(it, { listDepth, listIndent })).join("");
    case "list_item": {
      const marker = "-";
      const contIndent = listIndent + pad(marker.length + 1);
      const childIndent = pad(Math.min(contIndent.length, 12));
      const first = (tok.tokens ?? []).find((b) => b.type !== "space");
      const startsWithBlock = first !== undefined &&
        ["code", "blockquote", "hr", "table", "list"].includes(first.type);
      let needMarker = !startsWithBlock;
      const body = (tok.tokens ?? []).map((b) => {
        const out = render(b, { listDepth: listDepth + 1, listIndent: childIndent });
        // >>> the bug: every block child is returned untouched <<<
        if (["code", "blockquote", "hr", "table", "list", "space"].includes(b.type))
          return out;
        const lead = needMarker ? `${listIndent}${marker} ` : contIndent;
        needMarker = false;
        return out.split(NL)
          .map((l, i) => (i === 0 ? lead + l : l === "" ? l : contIndent + l))
          .join(NL);
      }).join("");
      return needMarker || startsWithBlock
        ? `${listIndent}${marker}${NL}${body.replace(/^\n+/, "")}`
        : body;
    }
    case "code":
      // >>> the bug: `listIndent` is never read here <<<
      return tok.text + NL;
    case "text":
      return tok.text + NL;
  }
  return "";
}

const item = (text, child) => ({
  type: "list_item",
  tokens: child ? [{ type: "text", text }, child] : [{ type: "text", text }],
});
const list = (...items) => ({ type: "list", items });

console.log(render(list(item("a", list(item("b", list({
  type: "list_item",
  tokens: [{ type: "text", text: "c" }, { type: "code", lang: "", text: "FOO BAR" }],
})))))));

Output:

- a
  - b
    - c
FOO BAR

FOO BAR lands at column 0 while its item sits at column 4.

Suggested fix

Give the code branch the same treatment the non-block children get: split the
highlighted body on newlines and prefix each non-empty line with the listIndent it
was handed. The same applies to blockquote, hr and table.

View original on GitHub ↗