agentleFS
koteitankoteitan/my-skills/skills/github-math-check/SKILL.md

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

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
  1. Very specific:

    never write $ anywhere inside math — not in \text{…}, not in \tag{…}

    line 34
  2. Very specific:

    Not every formula goes through the math renderer.

    line 46
  3. Very specific:

    put formulas that are not math in a code span.

    line 64
  4. Distinctive:

    when the file is generated, fix the generator and regenerate — not the output

    line 66
  5. Very specific:

    Read the HTML it returns: <pre lang="math"><code>…</code></pre> for a fence, $<code>…</code>$ for $…$ .

    line 120
  6. Very specific:

    Add a cache-busting query (?t=…) — GitHub serves the page cached otherwise, and you will keep reading an old revision.

    line 151
  7. Distinctive:

    Wait ~10 s after the push before fetching.

    line 181

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 filedelivered to the renderereffect
\\ (row break)\\\\begin{aligned} rows merge → Missing \end{aligned}
\\\\\\\same
\cr, \newlineunchanged✅ use these for row breaks
$\{x\}$${x}$braces vanish (Markdown ate the escape)
$`a<b`$$a&amp;lt;b$double-escaped → KaTeX sees &lt;; 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 fencecontents 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 → ```math fence (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 ```math fence 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 renderseffect
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)unchangedfine 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 nextunchangeda 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: 3

Scripts (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/YAPSS

scripts/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   : 0

Both 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.json

Read 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
EOF

Count 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

  1. 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.
  2. Method 2 on that formula. The delivered string tells you which transformation bit you.
  3. 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.