More than a decade ago, I switched workplaces and ecosystems. Gone was the Microsoft stack I knew, and in its place: Linux, open source, and this thing called Git that everyone swore by but nobody could explain clearly.
A colleague shared learngitbranching.js.org. It was a revelation. Instead of reading documentation or watching tutorials, I could play with branching, see the DAG update in real time, and build intuition through experimentation. To this day, I credit that tool for how I think about Git.
The Jujutsu Experiment
Fast forward to now. I’ve been building Hash, and as part of my constant learning journey, I figured it was time to try Jujutsu (jj). It’s the new darling of version control—started by Martin von Zweigbergk as a hobby project, with a fundamentally different mental model that promises to fix Git’s rough edges.
I figured Claude would help me do the heavy lifting and make this non-trivial migration smooth. I was wrong.
Claude is deeply biased toward Git. Even with explicit instructions in CLAUDE.md stating “this is a jj project,” it kept trying to break out—suggesting Git worktrees, recommending git stash, falling back to muscle memory that doesn’t apply. Every other prompt required course correction.
And then I realized: I wasn’t really learning jj either. It was monkey-see, monkey-not-even-do. Claude would suggest commands, I’d run them, and I had no idea why they worked or what mental model I should be building.
If You Want to Learn, Teach
There’s an old saying that the best way to learn something is to teach it. So I asked myself: what would a modern learngitbranching.js.org look like, but for jj?
From that question came JJ for Gitters—an interactive web tool that teaches Jujutsu through hands-on exercises with a live DAG visualization.
The Architecture of Truth
The most important design decision was philosophical: run the real binary.
Most educational coding tools simulate their target. They reimplement the logic in JavaScript, which is faster to build but inevitably drifts from reality. Edge cases get missed. Error messages don’t match. Behavior diverges in subtle ways that create bad habits.
JJ for Gitters takes a different approach. Every command you type runs against the actual jj binary in a sandboxed temporary directory. The output you see is real. The graph that updates is parsed from real jj log output. There’s no simulation layer to get wrong.
User types command → API route → child_process.spawn('jj', args) → Real jj execution
↓
Graph component ← Parse jj log output ← Engine returns GraphNode[] ← Sandbox directory
This “architecture of truth” means the tool can never teach you something that jj doesn’t actually do. Trust is paramount.
The Mental Model Shift
The tutorial levels are designed around the key mental shifts Git users need to make:
Level 1: The Working Copy is a Commit
In Git, you stage changes, then commit. In jj, your working copy is a commit—it just hasn’t been described yet. There’s no staging area. You edit files, run jj describe, and move on. The @ symbol in jj represents your current change, not a branch pointer.
Level 2: Bookmarks are Just Labels
Git branches feel like places you travel to. You “checkout” a branch, and you’re on it. Jj bookmarks are sticky notes you attach to commits after the fact. You create your commit chain first, then label what matters. Create first, organize later.
Level 3: History is Mutable by Default
In Git, editing history means scary rebasing with potential conflicts. In jj, you just jj edit @- to jump to a parent commit, make changes, and children automatically rebase. It’s the default workflow, not an advanced operation.
Level 4: The Safety Net
The killer feature: jj op log records every operation, and jj op revert can undo anything. Accidentally abandon a commit? Revert. Rebase went wrong? Revert. Nothing is permanent. The operation log is your time machine.
Technical Bits
The architecture bundles jj directly into the server:
- Next.js for the API routes and React frontend
- xterm.js for a real terminal experience (not a styled div)
- React Flow with dagre for automatic DAG layout
- jj binary bundled with the server, running in sandboxed sessions under
/tmp/jj-learner-sessions/{uuid}/
Each tutorial level is a config object with steps, initial setup commands, and validation functions that check whether you’ve completed the objective:
validate: ({ graph }: ValidationContext) => {
const workingCopy = graph.find(n => n.isWorkingCopy);
return !!workingCopy && workingCopy.description.includes("hello");
}
The command allowlist keeps things safe: only jj, ls, touch, echo, cat, mkdir, and git are permitted.
Dogfooding
The real test: am I actually learning jj by building this?
Yes. Forcing myself to articulate why jj works the way it does, designing exercises that build intuition, and validating that users can complete objectives—all of this has solidified my understanding far more than any documentation could.
I still catch Claude suggesting Git commands. But now I know why the jj way is different, and I can course-correct with confidence.
If you’re curious about jj, give JJ for Gitters a try. And if you have feedback on the pedagogy or want to contribute levels, I’d love to hear from you.
The best way to learn is still to teach.