I built my CV like a product: designed in Pencil, exported and published by custom Claude Code skills, with a real renderer bug debugged along the way.

I redesigned my CV recently. Not in Figma, not in Canva, not in a Word
template — in a .pen design file, versioned in git, exported to a PDF, and
pushed to my own assets CDN domain by a couple of scripts I don't even have to run
by hand. You can think of the whole thing as a tiny product, built side by side
with AI: design, code, and ops in one loop. Sounds nerdy? Probably — but don't
worry, I'm a dev who likes to stay in control, even when AI is doing the heavy
lifting. One bit of context up front: my whole setup here is the Claude Code
extension running inside Cursor — worth flagging now, because a bug I hit later
might be specific to that particular combo rather than something universal. So
let's dive in!
Ah, wait, just a sec — what's a .pen file? It's the design file for
Pencil, a vector editor, so the CV is a real design, not a
document template, living right there on my machine. You might ask why I didn't
just reach for Claude Design or one of the other trendy AI-powered design tools.
The answer is simple: I didn't want to leave my editor to hop into yet another web
app. I wanted to be in the loop — moving a box here, fixing a margin there by
hand when needed, alongside my other coding projects, on my own machine — automating the
boring parts from a single place.
Here's the project structure:
cv/
├── cv.pen # source design (Pencil) — the single source of truth
├── cv.pdf # exported vector output
├── resources/
│ ├── avatar.png # profile photo
│ └── personal_brand_logo.png # monogram
├── .claude/
│ └── skills/
│ ├── cv-pdf/ # /cv-pdf — export vector PDF + stamp links (more on this later)
│ │ ├── SKILL.md # the skill definition
│ │ └── scripts/build_cv_pdf.py
│ └── cv-upload/ # /cv-upload — push to R2 + purge edge cache (more on this later)
│ └── SKILL.md # the skill definition
├── CLAUDE.md # design system + working rules (the AI reads this every session)
├── .env # secrets, gitignored
└── README.md # the project README
Pencil ships an MCP (Model Context Protocol) server out of the box with its IDE
extensions for Cursor and VS Code. So the setup is genuinely just: install
the extension, open the .pen file, log in, and start designing. With the file
open in the editor, Claude Code detects it and
can drive the design directly — read the current layout, tweak an element, take a
screenshot, look, adjust. That screenshot-in-the-loop matters more than it sounds:
the model isn't designing blind, it's reacting to the rendered result the same way
I would.
What made this not slop:
resources/ folder — never regenerated, never replaced with a
placeholder.Treating the AI as a design pair — with guardrails — beats treating it as a
one-shot generator. The guardrails live in a CLAUDE.md the model reads every
session, which is really just the project's design doc doing double duty. If the
design system outgrows that single file, there's now a tidy convention for exactly
this: DESIGN.md — a markdown
file that hands a design system to coding agents, pairing machine-readable tokens
with the prose rationale behind them. You'd drop the tokens, fonts, and layout
rules into a dedicated DESIGN.md and pull it into CLAUDE.md, so the design
system lives in one focused doc the AI reads every time.
Two tasks happen on every update: export the PDF and publish it. Both have
sharp edges, so both became skills — small, named, reusable workflows Claude
Code can invoke (/cv-pdf, /cv-upload).
A skill is more than a script. It's encoded institutional memory: the steps
plus the gotchas you only learn by getting burned. For example, /cv-upload
doesn't just push to my Cloudflare R2 bucket — it then purges the Cloudflare edge cache,
because the domain caches the PDF for a few hours and without the purge the old file
serves for the rest of the afternoon while you swear at your browser. That lesson
is now baked into the skill so I never have to relearn it.
This is the part I'd most encourage other people to copy: when you find yourself explaining a fiddly multi-step process to an AI for the second time, turn it into a skill. You're building yourself a shortcut you'll reach for again and again!
Now for the fun part. When I went to export the PDF through Pencil's MCP, it threw this at me:
MCP error -32603: failed to execute tool call.
you are probably referencing the wrong .pen file
Rude! Nothing was wrong with the file: exporting that same file to PNG, through that same MCP server, worked perfectly. (Remember that setup caveat from the top — this might be specific to my exact combo, so don't read it as "Pencil is broken." The fun part is the chase, not the bug.) So I did what I'd do with any incident at work: stop guessing, start gathering evidence — with Claude Code doing most of the legwork.
Evidence #1 — the error is misleading. It turns out the MCP "server" is just a little program that passes my requests along to the real renderer inside the editor. And that "wrong .pen file" message? A generic catch-all it tacks onto any failure — friendly-looking, completely unhelpful.
Evidence #2 — read the source, then bisect. Next I cracked open the rendering code (it ships in a readable form) and noticed the PDF path does something the PNG path doesn't: to keep the exported text selectable, it tucks an invisible text layer into the file. So I had Claude Code export the page piece by piece — and every piece was happy on its own. Only when the whole main column went out as a single PDF page did it freeze, every single time. A hang that depends on the exact content, hiding inside a renderer I can't peek into.
So the verdict: this almost certainly wasn't my file or my config — just a misleading error and a bug I couldn't reach from where I sat. And I only got there by reading the source and measuring, instead of taking that error message at its word.
Pencil also ships a CLI built on the same engine but with a headless renderer. On a hunch that headless might dodge the in-app hang, I tried it:
npx -y @pencil.dev/cli@latest --in cv.pen --export /tmp/cv.pdf --export-type pdfSix seconds. A valid, vector PDF with selectable text. The headless renderer sidesteps whatever the in-editor one trips over.
It had one gap — like the native export, it embeds no clickable link
annotations. So I reworked /cv-pdf into a two-step pipeline: export the vector
PDF with the CLI, then stamp the links back on — contact details, the highlight
links, and the footer URL — with PyMuPDF, scaling the page to true A4 along the way. The result is strictly
better than where I started: selectable, searchable text and working links,
crisp at any zoom — instead of a flat image.
The bug became a feature upgrade. That happens more than you'd expect when you refuse to stop at the first workaround!
A quick aside on MCP vs CLI: for commands a model already knows by heart —
git, npx, curl, the stuff that fills its training data — a plain CLI is often
more token-efficient than an MCP server. The model just writes the command; no
one has to teach it the syntax. MCP can't lean on that: its tools are custom, so it
loads their full schema into context up front — often tens of thousands of tokens
before you ask anything (lazy tool-discovery is starting to claw that back). The
catch: a CLI needs a real place to run — my machine, or a sandbox the agent
controls — with the runtime, deps, and auth actually set up.
Thanks for reading! If this made you want to build your own tiny product, I'd love to hear what you build. 🚀