Cancelled GitKraken and Built My Own Git Client in Two Days

GitKraken was too heavy for my machine and my wallet
The annual GitKraken renewal email arrived and I looked at it with a special kind of fondness: the fondness you have for your electricity bill. I pay every year. For a Git client. That takes forever to open and eats RAM like Chrome with 40 tabs.
The worst part wasn't even the money, it was the wait. Click the icon, go grab a coffee, come back, and the thing is still loading. My machine handles big project builds all day, but it struggled to render a commit graph in Electron.
So I thought: what if instead of switching to another paid tool (or a free, crippled one), I just built my own? My way, lean, no annual fee.
GitCraque was born. This post tells you how.
Estimated reading time: 5 min

Two days of vibe coding and a pun I'm not giving up
So I built my own. It's called GitCraque, and it was basically two days of vibe coding from scratch until it became my daily Git client. No Electron, no annual subscription, no waiting for a splash screen.
The name? Look at GitKraken: a giant octopus, eight arms, lives at the bottom of the sea and never won a World Cup. Eight arms and zero trophies is a poor return for any squad. On the other side, there was a guy with the most indefensible haircut in football history, two goals in the final, and the nickname The Phenomenon. The choice made itself. I swapped the K for a C, the cephalopod for the number 9.
Yes, it's a tribute. Yes, it's a pun. No, we will not stop.
To test it, just one command, inside any repository:
npx gitcraque # inside the repo directory
npx gitcraque ~/code/project # or point to a path, like `git -C`
npx gitcraque --repo ~ --port 5271
npx gitcraque --no-open # don't open the browser
The repo is at https://github.com/frederico-kluser/GitCraque. And I'll say it right away: the project is growing, evolving, it's not finished. It's playing well, but it's still in season.
The line-up: a backend with exactly one dependency
Lean squad, no bloated roster and no payroll in node_modules. Here's the starting XI:
- Goalkeeper: Plain Node with
node:http. Holds the API line with no framework up front. No Express, no Fastify, just what ships in the box. - Defender:
child_process, marking thegitbinary tight, always with argv as an array. No shell interpolation in my team. - Full-back:
ws. The backend's only dependency. One. Total. - Midfield: React 19 + Vite, distributing play to the SPA.
- Number 10: Tailwind + Motion UI, the first touch and the flair.
- Winger:
@dnd-kit/core, drags the commit and leaves the marker behind. - Number 9: Hand-written SVG. The entire graph is
<circle>and<path>with cubic Bézier. No gitgraph library. The dribble is all ours.
And Electron? Didn't get the call-up. A Git client packed in Chromium is that player who shows up heavy, eats half the RAM, and takes forever to warm up. Not here.
Requirements to take the pitch: Node >= 22.13 (the node:sqlite drops the experimental flag from that version) and git on the PATH. Boots optional.
The features that made me forget GitKraken
Team's on the pitch, now here's what it does. Six things, in the order they won me over:
-
The history graph. The backend runs
git log --all --topo-orderand hands over the raw data; the front calculates the(X, Y)of each commit with its own algorithm, separating branch children from merge children so the routes never cross. The drawing is hand-written SVG,<circle>for commits and<path>with Bézier curves for branches, with window virtualization. Repo with tens of thousands of commits and the scroll stays smooth. No gitgraph library involved. The dribble is all ours. -
Worktrees without checkout. Click a worktree in the sidebar, the server does
process.chdir()to the absolute path and notifies via WebSocket. Nogit checkouthappens, nobody's working tree gets touched. It's swapping positions without flagging the fourth official. -
My favorite, and I've tested them all: delete a worktree, local branch, and remote branch with a single command. In GitKraken I used to do it in two separate commands, every time, and every time it annoyed me. Now it's one click. Seems silly, but it's the kind of daily friction that adds up until you cancel a subscription.
-
Drag-and-drop with intent. Dragging a commit onto a branch offers cherry-pick; branch onto branch opens a choice between merge and rebase. The raw command shows up before running, and anything that rewrites history requires press-and-hold: the button only gives in if you hold it. Time to think before you kick.
-
Push that never hangs. Trampoline model with
GIT_ASKPASSpointing to the Node script itself, which answers git via stdout. The token travels over a unix socket with a one-time nonce. Never on argv, never on disk. -
Picker when you open outside a repo. Screen with recents, scanning usual folders, and even
git initright where you're standing. Started outside a repo? Not an error, it's a scout.
Squashing commits without vim in your face
Every Git client that tries to do a "pretty" interactive rebase falls into one of two traps: either it emulates a whole terminal inside the UI (heavy, fragile, hideous), or it gives up and opens vim right in your face. I hate both. Git already knows how to do an interactive rebase on its own; the problem is it insists on opening an editor so you can manually edit git-rebase-todo.
Then it hit me: what if the "editor" was my own script?
Git respects the GIT_SEQUENCE_EDITOR variable. When it's set, it generates git-rebase-todo, calls the program you point it to with the file path, and waits for an exit 0 to continue. I inject GIT_SEQUENCE_EDITOR="node proxy-editor.mjs" and the script does the dirty work:
// server/proxy-editor.mjs
import { readFileSync, writeFileSync } from 'node:fs';
const todoPath = process.argv[2];
const targets = new Set(process.env.GITCRaque_SQUASH.split('\n'));
const lines = readFileSync(todoPath, 'utf8').split('\n');
const edited = lines.map((line, i) => {
const [action, hash] = line.split(' ');
if (action === 'pick' && i > 0 && targets.has(hash)) {
return line.replace(/^pick/, 'squash');
}
return line;
});
writeFileSync(todoPath, edited.join('\n'));
process.exit(0);
It reads the todo that git itself generated, swaps pick for squash on the lines you marked in the UI (keeping the first commit as pick, otherwise the whole rebase goes off the rails), writes it back, and exits with 0. Git applies the rewrite as if a human had edited the file with the utmost care.
Zero terminal emulation. Zero vim.
Three commits go in, one comes out. And nobody saw how.
The comeback: surviving when Chrome dumps your tab
Chrome has two ways to save resources with background tabs, and both hurt differently.
The first is freezing: task queues stop and the WebSocket goes half-open. readyState swears it's OPEN, but the connection died on the other side. It's the player standing on the pitch, out of the play.
The second is worse. Discarding: Chrome wipes the page from memory entirely and it comes back from scratch, like a first visit.
GitCraque handles all three comeback routes: visibilitychange, resume, and pageshow with persisted. And the view state snapshot happens when the tab hides, never on exit. Why? Because beforeunload and unload simply don't fire when the browser discards the tab. You set up the handler, trust it, and it never runs. I found out the fun way.
There's also a root boundary to catch exploding renders, and the auto-reload has a budget. A reload loop is worse than a broken screen: you can't even open devtools to investigate.
Knee rebuilt, World Cup artillery. It works.
What I learned testing everything with TDD
People asked me what the hard part was. Honestly? None of it was that complicated, and the reason is annoyingly simple: I tested everything as I went, and I tested it well. TDD from start to finish, plus end-to-end tests on top. Today npm test runs 472 tests: 319 for the server, 51 for the graph, 20 for drag-and-drop, 82 for the viewer. And there are 39 e2e checks outside that count.
There's a real trade-off here, and I learned it the hard way: run one test command at a time. The graph suite measures wall-clock proportions, so if you run it alongside another heavy process, it'll flag a failure that never actually happened. And there's no VAR here.
Security was a design decision, not an afterthought: the server only listens on 127.0.0.1, rejects requests with outside Host/Origin, and should never, ever be exposed to the network.
And let's be clear: the project is growing. It's not finished, it'll keep improving over time. Just like the Phenomenon after the knee, actually.
Try it out and tell me what broke
Enough reading. Go into one of your projects and run:
npx gitcraque
npm install -g gitcraque
cd ~/code/project && gitcraque
The view opens by itself in the browser, already lined up on the repository. No permanent install, no sign-up, no credit card.
A heads-up before you curse the project: npm i github:frederico-kluser/GitCraque doesn't work. The published package already includes the built SPA, and the build only runs on npm pack. From source, it's clone + npm run build.
Like it? Star the repo, open an issue, send a PR. Or don't even use it: get inspired and build your own. And share this post on Slack or WhatsApp with that person who still pays for a Git client every year. You know one.
Boots optional.