2026-08-11 · 12 min read · GitGraph
I built a native git graph so I could stop opening VS Code for one extension
GitGraph with Claude Code — Tauri, Rust lanes, safe git, and the stuff that almost bit me.
I kept doing this dumb loop: open VS Code, open Git Graph, look at a branch, close VS Code. The editor was a tax I was paying every time I just wanted a picture of the history.
So I built GitGraph — a small native app. Tauri 2, Rust on the bottom, React on top. macOS first, Windows and Linux in the same tree. MIT, solo, open source.
This is how I actually built it with Claude Code, not a polished “AI wrote my app” story. I drove. The model typed a lot. I still had to know when it was wrong.
What I wanted
- A real commit graph, not a filtered list that lies about parent edges
- Multiple repos in a sidebar, state restored when I bounce between them
- Stage / unstage / commit / stash without leaving the window
- Right-click refs and commits for the stuff I do every day
- Safety: opening a random folder must not run that repo’s hooks
That last one is non-negotiable. If your “git GUI” fires pre-commit from untrusted code, you’re shipping a footgun.
Stack choice (and why not Electron)
I wanted something that feels like a system app, not a Chrome window with git makeup. Tauri 2 was the sweet spot: Rust for process control and graph math, web UI for the list and canvas, small-ish binary.
Claude Code was useful for scaffolding the Tauri commands and the React shell. It was less useful for the graph correctness — that needed tests against real repos and a lot of “no, that edge should not be there.”
How I worked with Claude Code
Rough loop that worked for me:
1. Write the threat model and architecture in the repo (AGENTS.md, SECURITY.md) before dumping features. Agents (and future me) need rails. 2. Keep one IPC surface. All Tauri calls go through ipc.ts. The model loves to invent random invoke calls from components — I kept killing that. 3. Hardened git wrapper first. safe_cmd.rs disables hooks, scrubs hijack env vars, timeouts, validates refs. Then every command uses it. No “we’ll harden later.” 4. Graph as data, not as paint. Lane assignment and segments live in Rust. The canvas just draws what the backend already decided. 5. Fixture mode for UI. Generate fixture.json from a real repo, run the UI in the browser without Rust. Faster feedback when I’m just fighting CSS and virtualization.
I would open Claude Code in the repo, point it at the failing test or the broken lane, and say: fix this invariant, don’t invent a second graph model. When it drifted, the e2e tests that walk a real history caught it.
The hard parts
Filtering must remove commits, not leave ghost edges
If you filter branches and still draw edges into missing parents, the graph is a lie. The revwalk seed is the filter. Claude initially “hid” rows in the UI. Wrong layer. Fix belonged in the walk.
Search is highlight + jump, not filter
Filtering search results implies missing parents again. So search jumps with n / N and keeps the full graph. Same class of bug, different feature name.
Session restore is product, not polish
Per-repo scroll, selection, expanded commit, branch filter — across app restarts. Without it the app feels amnesiac and I go back to VS Code. Zustand + a single persist.ts that owns localStorage. One writer. No random localStorage.setItem in components.
Force push is a ceremony
--force-with-lease plus a typed confirmation. The model was happy to wire a normal push button. I was not.
Tests I actually trust
- Rust tests that hooks do not fire through the wrapper
- Graph e2e: every line leaving a row continues on the next; paging matches the walk
- Frontend vitest for layout, filter, menus, persist
If Claude changes something “small” in lanes, the e2e is the adult in the room.
What’s still alpha
No notarized releases yet. You build from source. Hunk staging, blame, interactive rebase — later. v0.1 is the graph + daily git verbs + not shooting yourself with hooks.
Open source
Repo: github.com/mubashirjamali101/gitgraph
If you only take one thing from this writeup: treat the repository as untrusted and the user as trusted. Wire that into the subprocess layer on day one. Claude Code will happily help you build features on top of a soft underbelly if you let it.
I built this because I was annoyed. Claude Code made the annoyance shippable in weeks instead of months. The taste and the safety rules were still on me.
