Skip to content

Instantly share code, notes, and snippets.

@chriswingler
Created April 23, 2026 22:50
Show Gist options
  • Select an option

  • Save chriswingler/e8511fda1956c456df012919e2513fcd to your computer and use it in GitHub Desktop.

Select an option

Save chriswingler/e8511fda1956c456df012919e2513fcd to your computer and use it in GitHub Desktop.
Claude Code (claude.ai/code) — userscript that adds Shiki syntax highlighting to bash output, "Used [tool]" bodies, command previews, and python heredocs inside bash. Works in verbose mode too.
// ==UserScript==
// @name Claude Code – Shiki syntax highlighting for tool blocks
// @namespace https://github.com/
// @version 1.0.0
// @description Adds Shiki syntax highlighting to bash output, "Used [tool]" bodies, the "$ ..." command previews on Bash cards, and python heredocs embedded inside bash commands. Tested against the Claude Code beta UI (claude.ai/code) in both normal and verbose modes.
// @match https://claude.ai/code/*
// @run-at document-idle
// @grant none
// ==/UserScript==
/*
Notes
-----
- The Code chat is heavily virtualized: only ~7 entries are mounted in the DOM at once,
so blocks unmount/remount as you scroll. The MutationObserver re-enhances on remount.
- "Used [tool]" bodies are rendered by an internal React component ("zm" in the bundle)
that exposes `{ tool, summary, diffMeta }` props. We read `tool` off the React fiber so
we can render `tool.input` / `tool.output` as separate, language-typed code blocks.
- Bash command previews ("$ ...") use a different class (`text-assistant-primary`) than
the bodies (`text-assistant-secondary`), and are siblings, not children, so we enhance
them with their own selector.
- `python3 -c "..."` style heredocs get their inner source rendered as Python instead of
being treated as one long bash string.
*/
(async () => {
const shiki = await import('https://esm.sh/shiki@1.0.0');
const theme = matchMedia('(prefers-color-scheme: dark)').matches
? 'github-dark' : 'github-light';
// Warm grammars so the first highlight per language is fast.
for (const lang of ['bash', 'python', 'json', 'sql', 'diff', 'powershell', 'console']) {
try { await shiki.codeToHtml('', { lang, theme }); } catch {}
}
const esc = s => String(s).replace(/[&<>"]/g,
c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
// Map well-known tool input keys to languages.
const langForKey = k =>
k === 'code' ? 'python'
: k === 'sql' ? 'sql'
: (k === 'shell' || k === 'command') ? 'bash'
: (k === 'patch' || k === 'diff') ? 'diff'
: null;
// For tool.output, sniff JSON, otherwise fall back to bash (handles shell/log output well).
const guessOutputLang = s => {
const t = (s || '').trim();
if (t.startsWith('{') || t.startsWith('[')) {
try { JSON.parse(t); return 'json'; } catch {}
}
return 'bash';
};
const hl = async (text, lang) =>
lang ? await shiki.codeToHtml(text, { lang, theme })
: `<pre>${esc(text)}</pre>`;
// python3 -c "<body>" / python -c '<body>' → render the inner body as Python.
const PY_RE = /(python3?\s+-c\s+)("([\s\S]*?)"|'([\s\S]*?)')/;
const hlBashWithPython = async (cmd) => {
const m = cmd.match(PY_RE);
if (!m) return await hl(cmd, 'bash');
const before = cmd.slice(0, m.index + m[1].length + 1); // include the opening quote
const py = m[3] !== undefined ? m[3] : m[4];
const after = cmd.slice(m.index + m[0].length - 1); // start at the closing quote
return (
`<div>${await hl(before, 'bash')}</div>` +
`<div style="border-left:2px solid rgba(255,255,255,0.08);padding-left:8px;margin:4px 0">` +
`${await hl(py, 'python')}` +
`</div>` +
`<div>${await hl(after, 'bash')}</div>`
);
};
// Enhancer #1 – the "Used [tool]" body div, driven from React fiber's `tool` prop.
const enhanceUsedTool = async (body) => {
const fk = Object.keys(body).find(k => k.startsWith('__reactFiber'));
if (!fk) return;
let f = body[fk];
while (f && !(f.memoizedProps && f.memoizedProps.tool)) f = f.return;
const tool = f?.memoizedProps?.tool;
if (!tool) return;
const sig = [
tool.id || '',
tool.status || '',
typeof tool.output === 'string' ? tool.output.length : 0,
tool.input ? Object.keys(tool.input).length : 0
].join('|');
if (body.dataset.shikiToolSig === sig) return;
body.dataset.shikiToolDone = '1';
body.dataset.shikiToolSig = sig;
const parts = [];
// Render input args
if (tool.input && typeof tool.input === 'object') {
for (const [k, v] of Object.entries(tool.input)) {
if (typeof v === 'string' && (v.includes('\n') || v.length > 60 || langForKey(k))) {
const lang = langForKey(k);
const html = (lang === 'bash' && PY_RE.test(v))
? await hlBashWithPython(v)
: await hl(v, lang);
parts.push(
`<div style="margin-top:8px">` +
`<div style="opacity:0.6;font-size:12px;margin-bottom:4px">${esc(k)}:</div>` +
`${html}` +
`</div>`
);
} else {
parts.push(
`<div><span style="opacity:0.6">${esc(k)}:</span> ` +
`${esc(typeof v === 'string' ? v : JSON.stringify(v))}</div>`
);
}
}
}
// Render output
if (typeof tool.output === 'string' && tool.output.length) {
parts.push(
`<div style="margin-top:8px;padding-top:8px;` +
`border-top:1px solid rgba(255,255,255,0.08)">` +
`<div style="opacity:0.6;font-size:12px;margin-bottom:4px">output:</div>` +
`${await hl(tool.output, guessOutputLang(tool.output))}` +
`</div>`
);
}
body.innerHTML = parts.join('');
body.querySelectorAll('pre').forEach(p => Object.assign(p.style, {
background: 'transparent', margin: 0, padding: 0,
whiteSpace: 'pre-wrap', wordBreak: 'break-word'
}));
};
// Enhancer #2 – the "$ ..." command preview at the top of every Bash card.
const enhancePreview = async (preview) => {
if (preview.dataset.shikiPreviewDone) return;
const text = preview.textContent;
if (!text) return;
preview.dataset.shikiPreviewDone = '1';
preview.dataset.shikiPreviewLen = String(text.length);
let prefix = '', cmd = text;
const m = text.match(/^(\s*\$\s)/);
if (m) { prefix = m[1]; cmd = text.slice(m[1].length); }
const html = PY_RE.test(cmd)
? await hlBashWithPython(cmd)
: await hl(cmd, 'bash');
preview.innerHTML =
(prefix ? `<span style="opacity:0.6">${esc(prefix.trim())} </span>` : '') + html;
preview.style.background = 'transparent';
preview.querySelectorAll('pre').forEach(p => Object.assign(p.style, {
background: 'transparent', margin: 0, padding: 0,
whiteSpace: 'pre-wrap', wordBreak: 'break-word', display: 'inline'
}));
};
// Selectors for both real shapes the chat uses for tool body text.
const TOOL_BODY_SELECTOR =
'div.text-assistant-secondary.whitespace-pre-wrap.break-words,' +
'div.text-assistant-secondary.whitespace-pre-wrap.break-all,' +
'pre.text-assistant-secondary.whitespace-pre-wrap';
const PREVIEW_SELECTOR =
'div.text-assistant-primary.break-words.whitespace-pre-wrap';
const scan = (root = document) => {
if (!root || !root.querySelectorAll) return;
root.querySelectorAll(TOOL_BODY_SELECTOR).forEach(enhanceUsedTool);
root.querySelectorAll(PREVIEW_SELECTOR).forEach(el => {
const t = el.textContent;
if (!t) return;
const inBashCard = el.parentElement?.classList.contains('text-code');
if (inBashCard || /^\s*\$\s/.test(t)) enhancePreview(el);
});
};
scan();
// Single observer that catches new nodes AND streamed character data.
new MutationObserver(muts => {
for (const m of muts) {
if (m.type === 'childList') {
for (const n of m.addedNodes) if (n.nodeType === 1) scan(n);
} else if (m.type === 'characterData') {
const host = m.target.parentElement?.closest(
'[data-shiki-tool-done],[data-shiki-preview-done]'
);
if (host) {
delete host.dataset.shikiToolDone;
delete host.dataset.shikiToolSig;
delete host.dataset.shikiPreviewDone;
delete host.dataset.shikiPreviewLen;
scan(host.parentElement || document);
} else {
const candidate = m.target.parentElement?.closest(
TOOL_BODY_SELECTOR + ',' + PREVIEW_SELECTOR
);
if (candidate) scan(candidate.parentElement || document);
}
}
}
}).observe(document.body, { subtree: true, childList: true, characterData: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment