I built a desktop image editor in five weeks with Claude Code
On September 4 I started a ComfyUI custom node: a canvas with layers and selection tools, so I could fix AI generations without leaving my workflow. On September 7 it became a desktop app. Today, five weeks after that fi
On September 4 I started a ComfyUI custom node: a canvas with layers and selection tools, so I could fix AI generations without leaving my workflow. On September 7 it became a desktop app. Today, five weeks after that first commit, Scumble is on its 43rd release, in the Microsoft Store, and edits 15,000-pixel pictures without freezing.
I didn't type most of that code. Claude Code did. My job was deciding what to build, testing it with my own pictures, and saying "no, not like that" a lot. This post is about how that collaboration actually worked: the setup, the habits that made it hold together, and the bugs the browser had waiting for us.
What Scumble is
A free, open-source (GPL-3.0) desktop editor for AI image editing. You select part of a picture (or draw boxes on an empty canvas), say what should be there, and the model's answer comes back as a layer of its own, colour-matched to its surroundings. Nothing is baked in until you flatten.
It renders wherever you already work: your own ComfyUI (local or remote), Comfy Cloud, or just an API key for FLUX 3 Image, FLUX.2, GPT Image, Nano Banana, Seedream and others. Object selection (SAM2), background removal and depth maps run inside the app. And every feature is also a command for AI agents.
The numbers, five weeks in
Releases 43 (0.1.0 on Sep 9, 0.1.44 on Oct 9)
Commits 632 in the app, 137 in the ComfyUI node
App code ~83,000 lines of JavaScript, ~2,900 lines of Rust
Test and tooling code ~83,000 lines (yes, about as much as the app)
Docs and plans ~43,000 lines of Markdown, 26 plan documents
MCP tools 109
The stack, in one breath
Electron, because I wanted the same Chromium on Windows and Linux and a Node process for exports and the MCP server. The editor is plain JavaScript on canvas and WebGL2 (filters, film looks, curves and LUTs are shaders, with a CPU fallback). Large pictures live in a tile engine: tiles in a SharedArrayBuffer, mipmaps built in workers, and the hot pixel kernels in Rust compiled to WebAssembly. SAM2, background removal and Depth Anything run through ONNX Runtime (DirectML on Windows). Generation goes either to ComfyUI, where a "recipe" is a workflow template the app fills in, or to provider adapters for the APIs.
The part I'm proudest of conceptually: every feature is a command. The menu calls commands, the plugin API calls commands, the built-in assistant calls commands, and Scumble --mcp exposes the same commands as an MCP server. One line in Claude Desktop's config and an agent can open, select, generate, colour-match and export, with the same undo stack as a human:
{
"mcpServers": {
"scumble": { "command": "C:\Program Files\Scumble\Scumble.exe", "args": ["--mcp"] }
}
}
How the work was organised
The model is fast. What kept five weeks of fast from turning into five weeks of mess was a handful of habits.
- CLAUDE.md is the project's memory Every session starts by reading CLAUDE.md at the repo root. It isn't a style guide. It's the state of the project:
Decisions already made, with the date and "do not reopen without the user". Electron, GPL-3.0, keys in the OS credential store, recipes instead of node graphs. Without this list, every third session wanted to relitigate something.
Where things stand: what shipped, what is built but not released, what waits for me.
Traps worth keeping: one line per bug that cost real time, so it never costs time twice (more on those below).
A numbered list of everything I've asked for. "Item 37" means the same thing in every session.
Long histories move to docs/HISTORY.md, open bugs live in docs/BUGS.md. The file stays readable because it is pruned, not appended to forever.
One section per session, then /clear
Early on, a session that built eight steps in a row ran its context up to 85 %, and quality dropped with it. The rule since then: one numbered step of a plan, its commit, a hand-over note, then /clear. It feels slower. It isn't.Plans before code, and "as built" after
Each release has a plan document (docs/PLAN_0_1_43.md and so on): what exists, the design, the steps, the traps. After a step is built, its row gets an "As built" note: what really happened, what differs from the plan, what was never run live. Those notes are what the next session reads, so nobody builds on an assumption.
One more rule came out of a mistake. A feature that changed how generated pixels land on the canvas got fully built, and only then did I read a one-sentence description of its effect and realise I didn't want it. Now any change to how pixels land starts with that sentence, in plain words, before any code.
- Tests against the real app, by risk Unit tests weren't enough for an editor whose bugs live in the GPU and the compositor. The main test tool is a gate runner: it starts a fresh instance of the real app with its own profile, drives it over the Chrome DevTools Protocol, and checks pixels, files and commands:
bash tools/run_gates.sh rel44 --offline --tiles on editor composite document export
At some point I told Claude the testing had become too heavy. Since then there are three tiers. Full (both rendering backends, a mutation round, a 15k measurement) only for what can lose or corrupt data: documents, autosave, Rust kernels, the compositor, export writers. Normal for features. Light for providers built from docs, skins and the manual. Fewer tests, in the places that matter.
- Agents with a job, not a swarm For bigger jobs Claude Code runs sub-agents: a builder on one file, a test writer against a stated API, and often a skeptic whose only job is to find what's wrong with the result. That last one paid for itself many times. What didn't work: lots of parallel workflows. They burned through my usage limit in an afternoon. One package at a time, one or two helpers, commit, then the next.
What the browser had waiting for us
The Traps worth keeping section of CLAUDE.md is the most honest part of the repo. A few favourites:
globalCompositeOperation = "copy" clears the whole canvas. Not the rectangle you draw. The entire canvas. In 0.1.8 this made a layer vanish whenever a small region was updated. Regional copy needs a clip() first:
ctx.save();
ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip();
ctx.globalCompositeOperation = "copy";
ctx.drawImage(src, x, y, w, h, x, y, w, h);
ctx.restore();
Premultiplied twice. Chromium applies UNPACK_PREMULTIPLY_ALPHA_WEBGL to typed-array uploads too. Tiles that were already premultiplied got premultiplied again, and semi-transparent edges went dark. The fix is boring: set both unpack switches explicitly before every upload.
A canvas 65,536 pixels wide draws nothing. No error, just nothing. 65,535 works. That's why everything that needs the whole picture asks the tile engine for bands instead of one big canvas.
requestAnimationFrame doesn't fire in a hidden window. Our test instances often run behind other windows, and waits based on rAF simply hung. Scripted waits use setTimeout, image loads wait for onload.
Too many getImageData calls switch off the GPU. After enough readbacks Chromium makes every new canvas a software canvas, for the whole document. A test that read pixels after every step was quietly benchmarking the slow path.
And one that wasn't the browser's fault: an early benchmark ran against the default profile, which is the profile I use for real work, and overwrote my open document. Since then every test run gets its own --user-data-dir. No exceptions.
Shipping
Releases go through GitHub: a v tag builds a draft in CI, and publishing it makes electron-updater offer it. Every release needs its CHANGELOG section first: the build fails without it, and the app shows the same text in its update dialog, so the notes are written for users, not for git.
Code signing was the expensive problem. A signing certificate costs money every year, and an unsigned installer triggers SmartScreen. The answer was the Microsoft Store: an MSIX package that Microsoft signs on submission, free. The GitHub installer and a portable zip are still there for people who prefer them. Linux gets an AppImage and a .deb from CI. macOS is next.
What I'd tell someone starting the same way
Write decisions down the moment you make them. The model forgets between sessions; the file doesn't.
Make it say the effect before the code. One plain sentence catches more wrong turns than a review does.
Test the real thing. Most of my bugs lived between the editor, the GPU and the OS, where mocks don't reach.
Keep a list of traps. Every bug that took an hour deserves one line, so it never takes an hour again.
Stay the one who judges. Claude wrote the code, but whether a colour match looks right, or a feature should exist at all, was always my call. That's the part I wouldn't hand over.
Scumble is young (0.1.x) and built by one person, so there are rough edges. If you try it, I'd love to hear what breaks.
GitHub: https://github.com/DenRakEiw/scumble
Microsoft Store: https://apps.microsoft.com/detail/9NDBTNNMXF2R
Videos and manual: https://www.denrakeiw.com/scumble
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.