kb://library/rendergitstable2026-06-16

rendergit — flatten a git repo to one HTML page (for humans and LLMs)

libraryeducationtoolingllm-contextcode-ingestioncxmlatlasinspectorkarpathy

rendergit — flatten a git repo to one HTML page

For anyone who has ever wanted to read a whole repo at once — or paste one into an LLM without 200 copy-paste rounds. rendergit clones a git repo and emits one static HTML file with two faces: a pretty, syntax-highlighted "human view" you scroll and Ctrl+F, and a raw "LLM view" — the entire codebase concatenated as one plain-text blob in Anthropic's CXML format, ready to drop into Claude or ChatGPT. It is ~600 lines of Python by Andrej Karpathy, "vibe coded a few months ago," BSD0-licensed, and quietly one of the most useful 30-minute tools in the LLM-era developer toolkit.

What it is

rendergit is a single-file CLI. You run:

rendergit https://github.com/karpathy/nanogpt

…and it (1) shallow-clones the repo to a temp dir, (2) walks every file, (3) renders them all into one .html file, and (4) opens that file in your browser. The output page has two modes, toggled by buttons at the top:

  • 👤 Human view — a two-column layout: a sticky sidebar table-of-contents (every file, with its byte size) on the left, and the full source of every text file inline on the right, syntax-highlighted, with Markdown files rendered as formatted HTML. Because it is one flat page, browser Ctrl+F searches the entire repo at once — no per-file jumping, no IDE.
  • 🤖 LLM view — the same repo, but as a single giant plain-text <textarea> in CXML (Claude XML) format. You select-all, copy, and paste the whole repository into an LLM in one shot.

That second mode is the reason it spread. The painful part of "ask an LLM about this codebase" was always assembling the context — opening dozens of files, copying each, hoping you didn't miss the one that mattered. rendergit turns that into one copy.

Install (uv recommended, since it's a tool, not a library):

uv tool install git+https://github.com/karpathy/rendergit
# or, from a clone:
pip install -e .

Why it matters

Three reasons, in increasing order of relevance to OpenAlice:

  1. It nails a real workflow with almost no code. No server, no database, no index, no embeddings — just clone → walk → format → one HTML file. It is the minimal viable "repo → LLM context" pipe, and its simplicity is the lesson.
  1. It standardised on CXML for whole-repo context. Wrapping each file in <document><source>…</source><document_content>…</document_content></document> is exactly the structure Anthropic recommends for feeding multiple documents to Claude: XML tags give the model clean boundaries so it doesn't blur file A into file B. rendergit made that the default packaging for "here is my repo."
  1. It is a sibling of the ingestion problem Atlas and the inspector already solve. Atlas walks ~60 repos, parses them, and serves a queryable graph; the [[graphify]]-derived AST call-graph hook is the structured end of that spectrum. rendergit is the unstructured end — no parsing, no symbols, just "give the LLM the raw bytes and let it read." Both are valid; knowing when to reach for which is the point (see the OpenAlice section).

It also pairs conceptually with the Karpathy [[llm-maintained-wiki]] idea: dump a corpus into an LLM, let the model do the reading and synthesis. rendergit is the "dump the corpus" half — a context packer, where the LLM-maintained-wiki is the consumer/curator.

How it works (real mechanics)

Everything lives in `rendergit.py` (~600 LOC). The pipeline is small and worth reading top to bottom. Here is the walkthrough by responsibility.

1. Data model — two dataclasses

@dataclass
class RenderDecision:
    include: bool
    reason: str          # "ok" | "binary" | "too_large" | "ignored"

@dataclass
class FileInfo:
    abspath: str
    rel: str             # path relative to repo root
    size: int
    decision: RenderDecision

Every file becomes a FileInfo, and decide_file() attaches a RenderDecision. The reason string is surfaced in the output, so when a file is missing from the human view you can see why it was skipped (too large, binary, ignored) — a small honesty touch that prevents silent omissions.

2. Clone — shallow, into a temp dir

def git_clone(repo_url, dst):
    subprocess.run(["git", "clone", "--depth", "1", repo_url, dst], check=True)

def git_head_commit(repo):
    return subprocess.run(["git", "rev-parse", "HEAD"], ...).stdout.strip()

--depth 1 means it grabs only the latest commit's tree, not history — fast, small, and enough for "render the current state." The HEAD hash is captured for the page header so you know exactly which snapshot you're looking at.

3. Walk + decide — collect_files() / decide_file()

collect_files() does root.rglob("*"), sorts the paths (stable output), and skips symlinks. Then decide_file() applies the three filters, in order:

  • Ignored — anything under .git/: "/.git/" in f"/{rel}/" or rel.startswith(".git/").
  • Too largesize > max_bytes, default MAX_DEFAULT_BYTES = 50 * 1024 (50 KB). Big files (lockfiles, generated bundles, data blobs) would dominate the page and the LLM context, so they're cut by default.
  • Binarylooks_binary() (next section).

A file survives only if it clears all three; otherwise it appears in the TOC greyed-out with its reason, but its bytes are not inlined.

4. Binary detection — looks_binary(), the pragmatic two-test heuristic

def looks_binary(path):
    if path.suffix.lower() in BINARY_EXTENSIONS:   # .png .jpg .pdf .mp3 .zip …
        return True
    chunk = open(path, "rb").read(8192)            # first 8 KB only
    if b"\x00" in chunk:                           # NUL byte ⇒ binary
        return True
    try:
        chunk.decode("utf-8")                      # can't decode ⇒ binary
    except UnicodeDecodeError:
        return True
    return False

This is the classic, robust trick: (a) an extension allow/deny list for the obvious cases, then (b) a content sniff of the first 8 KB — a NUL byte is the near-universal binary tell, and a failed UTF-8 decode catches the rest. Cheap, no libmagic dependency, wrong only on exotic edge cases. Worth stealing for any "is this text?" gate.

5. Human view — highlight_code(), render_markdown_text(), the TOC

The main render loop builds one big HTML string:

  • Syntax highlighting via Pygments. highlight_code() picks a lexer by filename (get_lexer_for_filename); on failure it falls back to TextLexer (render-as-plain rather than crash), then runs Pygments' HtmlFormatter. A single Pygments CSS block is emitted once in <head>.
  • Markdown files are rendered, not shown as source — render_markdown_text() runs the markdown library with the fenced-code extension, so a repo's README and docs read like docs.
  • Directory tree at the top: it tries the system tree command (try_tree_command()) and falls back to a pure-Python walker (generate_tree_fallback()) when tree isn't installed.
  • TOC sidebar: every file as an anchor link with its size; skipped files listed with their reason. Sticky CSS keeps it on-screen while you scroll.

The whole thing is one self-contained HTML file — inline CSS, no external assets, no JS framework. You can email it, commit it, or open it offline.

6. LLM view — generate_cxml_text()

def generate_cxml_text(files):
    out = ["<documents>"]
    for index, i in enumerate(files, start=1):
        out.append(f'<document index="{index}">')
        out.append(f"<source>{i.rel}</source>")
        out.append("<document_content>")
        out.append(read_text(i.abspath))
        out.append("</document_content>")
        out.append("</document>")
    out.append("</documents>")
    return "\n".join(out)

That's the entire "LLM packaging" innovation: concatenate every included file, each wrapped in a <document> with its path as <source>. The result is shoved into a <textarea> on the page so you can copy it without it being mangled by HTML rendering. The human view and the LLM view are generated from the same `FileInfo` list — same skip decisions, same set of files — so what you read is what the model reads.

7. CLI — argparse, then orchestrate

rendergit <repo_url> [-o OUT] [--max-bytes N] [--no-open]
  • positional repo_url
  • -o/--out output path (default: a temp .html)
  • --max-bytes override the 50 KB cutoff
  • --no-open don't auto-launch the browser (useful in scripts/CI)

main() wires it together: clone → collect_files → build HTML (human + CXML) → write file → webbrowser.open() unless --no-open → clean up the temp clone.

Key ideas & tradeoffs

  • Flat beats hierarchical for *reading*. A file tree is great for editing and terrible for getting the gist. One scrollable page + Ctrl+F is a genuinely better "what is this repo" experience than clicking through GitHub.
  • Same source of truth for human and machine. Because both views come from one filtered file list, there's no drift between "what I reviewed" and "what I asked the LLM about." Underrated correctness property.
  • Skip-with-a-reason, not skip-silently. Surfacing too_large / binary / ignored in the output means you never wonder whether a file was missed or deliberately cut. Cheap honesty; copy this pattern.
  • Zero infrastructure. No index, no server, no embeddings, no incremental cache. Re-run = re-clone = re-render. Perfect for one-shots; the opposite of what you want for a live, queryable index over 60 repos (that's Atlas's job).
  • CXML as a de-facto context format. Choosing Anthropic's document-tag convention means the output drops straight into Claude with the boundaries the model is trained to respect — and it's plain enough that other LLMs handle it fine too.

Honest caveats

  • "Vibe coded," not maintained. Karpathy's own README: he "vibe coded" it, uses it personally, but "won't be actively maintaining or supporting" it. Treat it as a sharp one-purpose tool, not a dependency you file issues against. The healthy community forks (rendergit-extended, rendergit-web, a D port, rendergit-lite) exist precisely because upstream is frozen.
  • No real token budgeting. The only size control is the per-file 50 KB cut. A large repo can still produce a CXML blob far past any model's context window. rendergit packs context; it does not measure or fit it. You are responsible for "will this fit?" — and for trimming with --max-bytes or by pointing it at a subdir.
  • Whole-repo, no selection. It renders everything that passes the filters. There's no include/exclude glob, no "just the src/ tree," no path allow-list beyond --max-bytes. For a focused question you often over-pack.
  • No structure, by design. It hands the LLM raw bytes — no symbol table, no call-graph, no cross-references. That's fine for "read and explain," but for "who calls agentic_loop?" or "blast radius of this change" you want a parsed graph ([[graphify]] / Atlas), not a flat dump. Different tool for a different question.
  • 50 KB default is aggressive. It silently excludes plenty of legitimately large source files in real codebases. Check the skipped-files list before trusting that a dump is "the whole repo," and raise --max-bytes when needed.
  • Public/clonable repos only, current-HEAD only. It shells out to git clone --depth 1, so private repos need your local git auth, and there's no history, no branch/tag/commit selector in the basic flow.

OpenAlice + Academy ladder

Where it fits in our stack. rendergit is the unstructured, one-shot end of the same "repo → understanding" axis Atlas lives on:

  • For Atlas/inspector ingestion — rendergit's looks_binary(), decide_file() skip-with-reason, the 8 KB NUL-byte sniff, and the shallow-clone-into-temp pattern are all directly reusable building blocks for any ingest walker. We don't need its output (Atlas produces a queryable index, not a static page), but its filtering heuristics are exactly the decisions an ingestor must make, written plainly in ~600 lines.
  • CXML as our "feed a whole repo to an LLM" format — when a sub-agent or a human wants to ask an LLM about a small repo wholesale (not query Atlas), the CXML <documents> packaging is the right shape. It's the manual escape hatch for the rare case where the structured path (atlas.code.search / callers / context) is overkill and you just want "read this whole small repo."
  • The decision rule (this is the load-bearing OpenAlice takeaway): per our query-Atlas-first convention, do not flatten a 60-repo ecosystem and paste it at an LLM — that's what Atlas's cron-fresh index exists to avoid. Reach for rendergit when the target is one small, possibly-external repo Atlas hasn't ingested (non-OpenAlice lab projects, a dependency you're vetting, a gist-sized tool) and you want a fast human+LLM read. For anything inside the ecosystem, Atlas already knows the answer.

Academy ladder (beginner → real depth):

  1. Run it once. uv tool install git+https://github.com/karpathy/rendergit then rendergit https://github.com/karpathy/nanogpt. Scroll the human view, Ctrl+F a function name, flip to the LLM view, copy the CXML.
  2. Feel the workflow. Paste that CXML into Claude and ask "explain this repo's architecture in 5 bullets." Notice you assembled zero context by hand. That's the whole pitch — internalise it.
  3. Read the source. It's one ~600-line file. Trace main()git_clonecollect_filesdecide_filegenerate_cxml_text. You'll understand a complete repo-ingestion pipeline in 20 minutes.
  4. Compare the two paradigms. Read [[graphify]] (structured AST/call-graph ingestion) next to rendergit (unstructured flat dump). Articulate when each wins. This is the core literacy for working on Atlas.
  5. Connect to the consumer side. Read [[llm-maintained-wiki]] — rendergit packs the corpus; the LLM-maintained-wiki reads and curates it. Together they sketch the "let the model read the code" loop that the Atlas knowledge library itself is an instance of.
  6. Steal the heuristics. If you ever touch the Atlas ingestor or the inspector's file walker, lift the binary sniff and skip-with-reason pattern verbatim. They're correct, dependency-free, and battle-tested.

See also

  • [[graphify]] — the structured counterpart: AST + tree-sitter call-graph extraction (the Atlas U7.1 ingestion hook). Read it directly against this.
  • [[llm-maintained-wiki]] — the consumer side of "dump a corpus at an LLM"; rendergit is the packer, that is the curator.

Atlas knowledge library · authored by ATLAS · 2026-06-16. Mechanics verified against `rendergit.py` and the README at the sources above; no behaviour was assumed where the source could be read.