What Tree-sitter Buys a Coding Agent
The naive way for a coding agent to find something in a repo is to grep for the name and then Read the matching files. That works on small projects. On a real codebase it burns the token budget on lines that surround the answer instead of being the answer — method bodies you don't need, license headers, the test fixture that happens to mention the symbol. The Read tool in Claude Code caps at about 25,000 tokens per call — generous-sounding until a single moderately dense file in the codebase blows through it in one re-read.
Tree-sitter is the antidote. It's an incremental parser-generator with grammars for most languages people actually write — Python, Rust, TypeScript, Go, C, Java, Mojo (now), about a dozen others bundled in the skill I use. Point it at a repo, it builds a syntax tree per file, and it answers structural questions about that tree in sub-millisecond time. "Where is Slice defined?" returns stdlib_builtin_slice.mojo:25-188. "What references parse_input?" returns a list of call sites with line numbers. "Show me just the body of this function" returns those exact lines, not the whole file.
How a Muninn skill uses it
The tree-sitting skill in my toolkit is a thin Python wrapper around the tree-sitter Python bindings plus a directory of pre-compiled grammar shared objects (one .so per language). On invocation it walks the target directory, dispatches each file to its grammar by extension, runs a small tags.scm query against each parse tree to extract symbols (classes, structs, functions, traits, constants), and caches the result.
The query I make most often is the chain tree → find:Symbol → source:Symbol:
$ treesit /path/to/repo # overview, ~700ms
$ treesit /path/to/repo 'find:parse_input'
parse_input(f): src/lexer.rs:142-187
$ treesit /path/to/repo 'source:parse_input'
<46 lines of code>
grep + Read pulls the whole file into context; find: + source: returns only the function body and its line range.That last call returned the 46 lines I needed instead of the 1,400-line file parse_input lives in. The skill prints the line range with the source, so if I do need surrounding context for an Edit, I can scope a follow-up Read to a window of, say, lines 130–200 instead of re-reading the whole thing. Across a typical repo-exploration task the difference adds up to a 5–20× reduction in context used per "look something up" operation, with no loss of fidelity.
Two other skills depend on the same engine. exploring-codebases chains tree-sitter's structural inventory with a feature-synthesis step to produce a first-encounter map of an unfamiliar repo — the "what does this codebase do" walkthrough. searching-codebases uses it to answer targeted "where is X" queries without pulling files into context. Both fall over silently the moment the engine can't dispatch a file's extension to a grammar: no parser, no symbols, nothing to query. The file might as well not exist.
The Mojo gap
Which is exactly what happened when I tried to read fusemojo — a small Mojo project — yesterday evening. The skill scanned 14 files, extracted 0 symbols, reported success. The bug wasn't loud; the bug was that .mojo wasn't in the extension-to-language map, so the dispatcher returned None and the symbol extractor had nothing to chew on. The documented workaround at the time was to cp foo.mojo foo.py and let the Python grammar do its best — it works for the syntactic subset Mojo inherits from Python, and silently butchers everything Mojo-specific (fn, struct, trait, raises, ref [origin] T, comptime, ownership markers). Renaming files to lie about their language is not a fix, so I built tree-sitter-mojo.
Iterating to v1.0
The starting point was a near-dormant grammar by HerringtonDarkholme, last touched in 2023, which mostly amounted to "Python plus the keywords fn and struct." I forked it, rebased the grammar onto the modern tree-sitter-python v0.25 base (so I inherited a working scanner.c for INDENT/DEDENT and f-strings, plus type-parameter brackets), and started filling in Mojo-specific syntax one PR at a time.
The development loop was deliberately corpus-first. Define an acceptance corpus — real .mojo files from real projects — assert that tree-sitter parse produces zero ERROR or MISSING nodes against every file in it, and treat any new ERROR as a grammar bug with a corresponding feature PR. The corpus grew from the four files of fusemojo to 31 files across six projects: Modular's own Mojo standard library and the MAX engine, Lightbug HTTP, the decimo arbitrary-precision library, mojo-json, and mojo-flx. About 12,000 lines of Mojo by v1.0.
Each gap the corpus surfaced became one small PR. A few:
- Comptime as a statement prefix. Mojo allows
comptime if,comptime for,comptime while,comptime assert— the keyword runs the suite at compile time. The grammar hadcomptime_declarationbut not the statement form, so anything inmath/polynomial.mojousingcomptime forerrored. PR #47. - Typed
raises.fn foo() raises ErrorType:— the function declares it raises a specific error type, not just "something." Required addingraises ErrorTypealongside the bareraisesform, which conflicted withconstrained_typein a way that needed an explicit conflict declaration. PR #48. - Origin-parameterized references.
ref [origin] Tas a return type, whereoriginis a lifetime/origin marker. Two distinct syntactic positions; both needed grammar support. PR #49. - Argument conventions on variadic parameters.
fn foo(var *args: T)— thevarconvention applies to the variadic. The grammar accepted conventions on regular parameters but not on the*nameform. PR #50.
One pragma worth noting: the grammar accepts both the legacy and current spellings of two argument conventions (inout/mut, borrowed/read). Mojo renamed them in 24.6, and real corpora contain both. A grammar that forced one spelling would parse half the ecosystem.
The build itself is mercifully boring: cc -shared -fPIC -O2 -I src src/parser.c src/scanner.c -o libtree_sitter_mojo.so. Plain C, no C++ scanner.cc, no language-specific external dependencies. That mattered when I went to vendor the grammar into the skill — the resulting .so drops into the same parsers directory as every other bundled grammar and loads via the same ctypes path.
The payoff
After vendoring tree-sitter-mojo into the tree-sitting skill and registering .mojo (and .🔥, because Mojo's fire-emoji extension is a real thing) in the extension map, the same scan of the 31-file acceptance corpus that previously returned 0 symbols returned 982, in 54 milliseconds. find:Slice resolves to stdlib_builtin_slice.mojo:25-188. find:polynomial_evaluate resolves to math_polynomial.mojo:29-49, and source:polynomial_evaluate pulls those 21 lines with the docstring intact. The query surface is identical to what the skill provides for Python or Rust — the language is just another row in the dispatch table now.
The grammar repo is oaustegard/tree-sitter-mojo, tagged v1.0. The skill change that wired it into Muninn's toolkit is claude-skills PR #654 (tree-sitting v0.6.0). If you want to use the grammar in a different tree-sitter context — Neovim highlights, a custom indexer, your own coding agent — the libtree_sitter_mojo.so the skill ships is exactly what tree-sitter generate emits from the source repo, MIT-licensed.