Building Procreate Brushes with AI — A Process Story
BrushWorxs Lab / Process Notes What started as a simple question — can Claude build Procreate brushes? — turned into one of the more satisfying deep-dives we've run in the lab.
BrushWorxs Lab / Process Notes
Download the Claude SKILL file.
What started as a simple question — can Claude build Procreate brushes? — turned into one of the more satisfying deep-dives we've run in the lab.
The short answer is yes. The longer answer involves binary plists, reverse-engineered class names, and a flat ZIP structure that turned out to be the culprit behind three weeks of brushes that imported beautifully and painted absolutely nothing.
The Feasibility Question
Before writing a line of code, we did the research. Procreate's .brush format is a ZIP archive containing a Brush.archive — a binary property list encoded in Apple's NSKeyedArchiver format — alongside shape and grain PNGs. The schema is not publicly documented, but the community has reverse-engineered most of it. Libraries like bpylist2 handle the archiver format in Python. We decided it was buildable, structured the approach around a two-tier system (shape/grain generation + archive assembly), and wrote a formal skill file to capture the workflow.
The First Brushes
The watercolor brush we designed first — hot press paper texture, loaded mop-brush tip shape, abundant wet-on-wet flow — looked great on paper. The shape PNG had organic radial feathering radiating from a soft Gaussian core. The grain was a fine micro-texture with barely-perceptible roller bands and pooling zones, sitting around 200/255 brightness — the smooth, calendared feel of actual hot press. We dialed in 30 parameters for wet-mix behavior, pressure-to-opacity mapping, and taper.
Every brush imported into Procreate. Every brush showed up in the library. Not one of them painted.
What We Got Wrong
The diagnosis took several rounds. We tried four different class names (Brush, VKBrush, PBrush, eventually discovering the real one is SilicaBrush). We tried bpylist2 for proper NSKeyedArchiver encoding. We tried raw binary patching to avoid plistlib's round-trip issues. We tried setting shapeInverted to True and False. We tried nulling the bundled shape paths, setting them to empty strings, leaving them alone.
The breakthrough came from a real exported brush. The user shared a Brush.archive from Procreate's own Scopus brush, which let us read the true key names, the real class hierarchy, and the actual parameter structure. The class name was SilicaBrush with parent classes ValkyrieBrush → NSObject. The shape convention with shapeInverted: True meant our black-center shapes were being double-inverted to transparent rings. The parameter names were entirely different from community guesses — plotSpacing, not spacing; dynamicsPressureOpacity, not opacityPressure.
We fixed all of it. The brushes still didn't paint.
The actual bug was in the ZIP structure. We had been wrapping all files inside a named subdirectory — MyBrush/Brush.archive, MyBrush/Shape.png — when Procreate requires a completely flat layout with files at the archive root. That single structural mistake caused every import to silently fail at activation.
There was a second bug underneath it: Scopus is a built-in Procreate brush, and its archive references internal library paths (Procreate_Library/Scopus_Shape.png) that only resolve inside Procreate's own rendering context. They cannot be resolved during re-import. The fix was using a user-created blank brush as the seed archive — one that has null shape/grain paths and simply reads Shape.png and Grain.png from the ZIP by convention.
What Works Now
With the flat ZIP structure and a proper user-created seed archive, the pipeline is clean. plistlib can round-trip user-created archives reliably (they only contain simple sub-objects — ValkyrieMagnitudinalCurve, NSDate, NSArray). We set parameters via standard Python dict modification, null the bundled shape path, and package everything flat. The brush imports, shows the stroke preview, and paints on first try.
The hot press watercolor brush — the one that started all of this — works. Soft blooming edges, pressure-sensitive opacity, abundant wet flow, a grain that whispers rather than shouts. The shape is a loaded mop tip with radial hair feathering and an organic bloom halo. It took longer than expected to get there, but the process produced something we understand completely, and a reusable skill file that captures every hard-won detail for the next one.
The lesson, as usual: the format is always more opinionated than the documentation suggests.