github-math-check
Skill1 starsChanged 3 months agoMITView on GitHub
Verify that math in a Markdown file actually renders on GitHub. Use when a formula renders locally (KaTeX/MathJax/VS Code) but breaks on github.com — "Missing \end{aligned}", vanished braces, stray commas — or before publishing math-heavy Markdown. Decodes the exact string GitHub hands to its client-side renderer, so the check is measurement, not guesswork. Also covers formulas written as plain text (no math renderer, e.g. generated tables of ordinal notations), where `*` and `_` turn into italics.
- Installs packages
- Sends data out
In this repository15 other files it gives its agents.
- autonomy-statSkill
- check-usageSkill
- compact-unfreezeSkill
- edit-statuslineSkill
- formalization-statement-fidelity-auditSkill
- get-contextSkill
- heart-beatSkill
- manim-ttsSkill
- mermaidSkill
- my-github-md-ruleSkill
- nostrSkill
- sessiondbSkill
- use-bmsSkill
- use-fandomSkill
- webapp-defaultsSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 2 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 34
never write $ anywhere inside math — not in \text{…}, not in \tag{…}
- Very specific: line 46
Not every formula goes through the math renderer.
- Very specific: line 64
put formulas that are not math in a code span.
- Distinctive: line 66
when the file is generated, fix the generator and regenerate — not the output
- Very specific: line 120
Read the HTML it returns: <pre lang="math"><code>…</code></pre> for a fence, $<code>…</code>$ for
$…$. - Very specific: line 151
Add a cache-busting query (?t=…) — GitHub serves the page cached otherwise, and you will keep reading an old revision.
- Distinctive: line 181
Wait ~10 s after the push before fetching.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
github-math-check
Rendering math locally proves nothing about GitHub. GitHub transforms the math text on its way to the client-side renderer, and the transformed string is what fails. Always measure the delivered string.
Known transformations (measured on live pages, 2026-07)
| written in the file | delivered to the renderer | effect |
\\ (row break) | \\\ | \begin{aligned} rows merge → Missing \end{aligned} |
\\\\ | \\\ | same |
\cr, \newline | unchanged | ✅ use these for row breaks |
$\{x\}$ | ${x}$ | braces vanish (Markdown ate the escape) |
$`a<b`$ | $a&lt;b$ | double-escaped → KaTeX sees <; write \lt / \gt |
\tag{$x$}, \text{$x$} | \tag{\$x\$} | $ is escaped even inside a fence → math commands land in text mode |
$a \, b$ | $a , b$ | a stray comma is rendered |
$`\{x\}`$, $`a \, b`$ | unchanged | ✅ code span is not escaped |
```math fence | contents kept, except \\ | ✅ for display math |
- item + indented ```math fence | <pre lang="math">, not upgraded to <math-renderer> | display math inside a list item shows raw LaTeX |
[^1]: … $\x`$ …` | not upgraded to <math-renderer> | math inside a footnote definition is not rendered |
So the safe forms are:
- display math →
```mathfence (not$$ … $$) - inline math →
$`…`$(not bare$…$) - row breaks →
\cr(never\\) <and>in inline math →\lt,\gt(inside a fence they are fine)- never write
$anywhere inside math — not in\text{…}, not in\tag{…} - math inside a list item or a footnote → inline
$`…`$only; a```mathfence there is served as a plain code block. For a long formula use$`\begin{aligned} … \cr … \end{aligned}`$on its own line.
Counting catches both silently: when check-github.js reports fewer formulas than
check-local.js, the difference is unrendered math, not a false alarm — the page shows no
error, just raw LaTeX. Diff the two formula lists to find which ones. (Measured 2026-08 on
one file: 8 lost to list items, 5 to footnotes; both were invisible to the error count.)
Plain-text formulas: * and _ turn into italics
Not every formula goes through the math renderer. Text such as psi(W^(W*w)), a*b or
f(_a) written outside math and outside code — typically a generated table of notations —
is plain Markdown, and a pair of * or _ becomes emphasis: the text between them is set
in italics and the delimiters disappear. Nothing reports an error. Measured with method 1,
2026-09:
| written (plain text) | GitHub renders | effect |
psi(W^(W*w)) = psi(psi_1(W^2*w)) | psi(W^(W<em>w)) = psi(psi_1(W^2</em>w)) | italics, both * vanish |
psi(W_2+W*w) | unchanged | fine while it is the only * in the cell |
f(_a) + g(b_), (_c_) | f(<em>a) + g(b</em>), (<em>c</em>) | _ next to ( ) or a space opens and closes emphasis |
psi_1(W_2) + W_w + psi_W_(w+1)(x) | unchanged | _ between two letters or digits never does |
*a in one cell, b* in the next | unchanged | a table cell is parsed on its own |
`psi(W^(W*w)) = …` | <code> | ✅ code span |
psi(W^(W\*w)) = psi(psi_1(W^2\*w)) | * kept | ✅ escaped, but every * and _ needs it |
So:
- put formulas that are not math in a code span; inside a table cell a
|still has to be written\| - when the file is generated, fix the generator and regenerate — not the output
scripts/check-emphasis.js finds them. For every table cell (or line) it reports a * or
_ that can open emphasis followed by one that can close it, by the CommonMark flanking
rules. Code spans, fences, \* \_ and **bold** runs are skipped; emphasis across a line
break within a paragraph is not detected.
node ~/.claude/skills/github-math-check/scripts/check-emphasis.js docs
# docs/table.md:5: | A star pair | psi(W^(W*w)) = psi(psi_1(W^2*w)) |
# docs/table.md:10: | F _ after ( | f(_a) + g(b_) |
# docs/table.md:15: H paragraph: W*w + W*2 and a_(b) + (_c_)
# files : 1
# errors: 3Scripts (start here)
scripts/check-local.js — before pushing. Renders every formula of the given files (or
every *.md of a directory) with KaTeX and enforces the safe forms above.
npm install -g katex # once
node ~/.claude/skills/github-math-check/scripts/check-local.js md/YAPSSscripts/check-github.js — after pushing, and this is the one that decides. It fetches
the file's rendered payload from GitHub, pulls out every <math-renderer> body — the
exact string handed to the browser's renderer — and runs each through KaTeX.
node ~/.claude/skills/github-math-check/scripts/check-github.js \
https://github.com/OWNER/REPO/blob/BRANCH/path/File.md
# url : ...
# formulas : 2178 (84 display, 2094 inline)
# errors : 0Both exit non-zero on failure, so they can gate a commit. Green locally does not imply green on GitHub — always run the second one on the pushed file.
Implementation note: request the blob URL with Accept: application/json and read
payload.codeViewBlobRoute.richText. The plain HTML page sets richTextTruncated: true
and richText: null for large files; the JSON response is never truncated.
Method 1 — GitHub's Markdown API (fast, no push)
Shows how GitHub converts a snippet. Good for comparing two ways of writing something.
printf '%s' '{"text":"```math\n\\begin{aligned}\nA &= 1,\\cr\nB &= 2.\n\\end{aligned}\n```\n"}' > /tmp/p.json
curl -s -X POST https://api.github.com/markdown -H "Content-Type: application/json" --data @/tmp/p.jsonRead the HTML it returns: <pre lang="math"><code>…</code></pre> for a fence,
$<code>…</code>$ for $`…`$. Check that backslashes and escapes survived.
⚠ The API does not reproduce the \\ → \\\ step, which happens in the blob-page
pipeline. Use method 2 for that.
Method 2 — decode what the live page delivers (authoritative)
curl -s --max-time 40 "https://github.com/OWNER/REPO/blob/BRANCH/PATH.md?t=$(date +%s)" -o /tmp/gh.html
python3 - <<'EOF'
import re, json
s = open('/tmp/gh.html', encoding='utf-8', errors='replace').read()
for b in re.findall(r'<script type="application/json"[^>]*>(.*?)</script>', s, re.S):
if 'aligned' not in b: # or any marker from the formula you care about
continue
def walk(o):
if isinstance(o, str): yield o
elif isinstance(o, dict):
for v in o.values(): yield from walk(v)
elif isinstance(o, list):
for v in o: yield from walk(v)
for t in walk(json.loads(b)):
i = t.find('begin{aligned}')
if i >= 0:
print(repr(t[i:i+200])) # <- the exact string the renderer receives
raise SystemExit
EOFCount the backslashes in the output. ,\cr is right; ,\\\ is the bug.
Add a cache-busting query (?t=…) — GitHub serves the page cached otherwise, and you
will keep reading an old revision. Confirm the revision by grepping the page for the
commit sha.
The blob page carries the math as <math-renderer class="js-display-math">$$…$$</math-renderer>;
the error text (Missing \end{aligned}) is produced in the browser, so it is not in
the served HTML. Do not conclude "no error" from its absence — inspect the string.
Method 3 — probe file (when comparing several spellings)
Push a throwaway file with the variants side by side, decode it with method 2, then delete it. This is how the table above was measured.
## A: fence + double backslash
```math
\begin{aligned}
A &= 1,\\
B &= 2.
\end{aligned}B: fence + \cr
\begin{aligned}
A &= 1,\cr
B &= 2.
\end{aligned}
Wait ~10 s after the push before fetching.
## Local pre-check (necessary, not sufficient)
Render every formula with KaTeX — the same engine GitHub uses — to catch genuine LaTeX
errors before looking at GitHub:
```bash
npm install katex
node -e '
const katex=require("katex"), fs=require("fs");
let bad=0;
for (const f of fs.readdirSync(".").filter(x=>x.endsWith(".md"))) {
const lines=fs.readFileSync(f,"utf8").split("\n"); let i=0; const rest=[];
while (i<lines.length) { // ```math fences
if (lines[i].trim()==="```math") {
let j=i+1, body=[];
while (j<lines.length && lines[j].trim()!=="```") body.push(lines[j++]);
try { katex.renderToString(body.join("\n"),{displayMode:true,throwOnError:true}); }
catch(e){ bad++; console.log(f,i+1,e.message.slice(0,70)); }
i=j+1; continue;
}
rest.push(lines[i++]);
}
const j=rest.join("\n"); // $`...`$ inline
for (const m of j.matchAll(/\$`([^`]+)`\$/g)) {
try { katex.renderToString(m[1],{displayMode:false,throwOnError:true}); }
catch(e){ bad++; console.log(f,e.message.slice(0,70)); }
}
const stray=j.replace(/\$`[^`]+`\$/g,"").match(/\$/g);
if (stray) { bad+=stray.length; console.log(f,"unprotected $ x"+stray.length); }
const rows=fs.readFileSync(f,"utf8").match(/\\\\(?!\\)/g);
if (rows) { bad+=rows.length; console.log(f,"use \\cr not \\\\ x"+rows.length); }
}
console.log("errors:",bad); process.exit(bad?1:0);'A green run here plus method 2 on one representative formula is enough to publish.
Order of work when a formula is reported broken
- Fetch the raw file (
raw.githubusercontent.com/...) and confirm which revision the report is about — the reader may be looking at a cached page of an older commit. - Method 2 on that formula. The delivered string tells you which transformation bit you.
- Fix the spelling per the table, re-push, and re-measure with method 2. Do not report it fixed on the strength of a local render.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

