Skip to content
Go back

Your Coding Agent Can't Draw

By SumGuy 13 min read
Your Coding Agent Can't Draw
Contents

The Grey Rectangle At The End Of Every Project

Ask a coding agent to build a landing page and watch what happens. It scaffolds the project, writes the components, wires up the routes, adds tests that actually pass, and drops a README that reads like a person wrote it. Then you open the browser and the hero image is a flat grey box. Or public/og-image.png doesn’t exist. Or somebody, at some point, gave up and dropped in the same stock photo of a laptop and a coffee cup that’s been on every SaaS landing page since 2019.

The gap is structural, and it has nothing to do with the agent slacking off. A coding agent operates in exactly two moves: write a file, run a shell command. Both of those are text in, text out. A PNG is neither. So the same loop that writes flawless TypeScript has no way to also produce the header image that TypeScript ships with, and it papers over the gap with a placeholder and moves to the next ticket. You end up doing image generation by hand, which is the one part of the pipeline nobody automated.

There are exactly two honest ways to close that gap without opening an image editor yourself: point at a GPU, or point at a subscription you’re already paying for. Both work. Neither is free in the way you’d hope.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/…

Option One: Feed It To Your Own GPU

This blog generates every featured image this way, and everything below comes from running that setup daily. The hardware is one 8GB GPU, shared between a local LLM and a ComfyUI instance that do not fit in memory at the same time. A gateway in front of ComfyUI cold-starts the container on demand. The first request of the day usually times out, gets refused, or comes back with a 5xx, and that’s normal behaviour for a cold start.

The generator script that calls it waits 180 seconds for the cold start, then polls the job for up to 9 minutes, and retries submits that fail mid spin-up. Output comes back at whatever the model wants to give, then gets resized locally to 1200x630 with Lanczos because nothing in the pipeline hits that aspect ratio on the first try.

So, the tradeoff. Once the hardware is paid for, every image is free, private, and works with the network cable pulled out of the wall. It’s also a machine that has to exist, a queue you babysit, and a cold start tax you pay on the first image of every session. It’s good at editorial illustration and bad at putting readable text inside an image, which rules out a whole category of diagrams and quote cards without a second editing pass.

If you already run a homelab GPU box, this is close to free. If you don’t, buying one to solve “my README has no picture” is like renting a forklift to move one couch. Technically it works. Your neighbors will have questions.

Option Two: The Subscription You’re Already Paying For

If you pay for a ChatGPT plan, image generation is sitting inside it, unused for anything except the chat window. That gap is what pushed me to write subpixel, a CLI and MCP server that drives image generation through your existing login instead of a GPU or a separate image API key. I wrote it, so grade the enthusiasm accordingly.

Before you install anything, read this part.

subpixel drives an undocumented endpoint. It talks to chatgpt.com/backend-api/codex, the same private API the Codex CLI uses, authenticated with your personal ChatGPT login rather than a published API key. That means:

  • It’s undocumented. OpenAI can change or shut it off with no notice, and subpixel will break the day they do.
  • Don’t put it behind a public-facing service. No web endpoint, no bot, no serving generated images to strangers on somebody else’s traffic.
  • It rides your personal subscription. If you run it at volume against your own account, the consequences land on your account.

With that on the table, the mechanics.

subpixel is an npm package, currently version 0.3.0, published 2026-09-16. The repository went public on 2026-09-14. It is days old as I write this, so treat it like new software: read the changelog before you upgrade, and don’t build a production pipeline on top of it yet.

It needs Node 24 or newer. The package enforces that in engines, so check node --version before you install anything else.

Terminal window
npm install -g subpixel
npm install -g @openai/codex
codex login

Notice there’s no image API key anywhere in that sequence. subpixel reads the auth.json file that codex login writes, at $CODEX_HOME/auth.json if you set that variable and ~/.codex/auth.json otherwise, so authentication rides on the Codex CLI instead of a key you have to generate, store, and rotate yourself. Both spx and subpixel work as the binary name, they’re aliases for the same tool. Runtime dependencies are yaml, commander, and @modelcontextprotocol/sdk. The sharp package is an optional peer dependency, only needed for --exact-size, --transparent, --variants, and the spx icons command. If you don’t touch those, you don’t need it.

Check your setup before you generate anything:

Terminal window
spx doctor

spx doctor reports your credentials, the driver model, whether sharp is installed, and which agent harnesses are wired up. Only the credentials line drives the exit code: 1 means you are not logged in, 0 means you are. The other rows are advisory, so read them rather than trusting the exit status alone.

Basic generation looks like this:

Terminal window
spx generate "wide banner, flat vector, teal and slate" > hero-path.txt

The detail that makes this scriptable: stdout is the written file path, one line, nothing else. Every log message, progress note, and warning goes to stderr instead. That means you can pipe the output straight into a build step or a Makefile target without parsing anything. Progress goes to stderr too, so a multi-image run tells you where it is rather than sitting silent for two minutes. A terminal gets one rewritten line, a pipe gets one line per event, and --quiet turns it off.

Generation is content-addressed. Ask for the same prompt twice and you get the same file back instead of a second charge against your account. Every image also gets a manifest written beside it: hero.png comes with hero.png.json, recording the prompt, the model, and the settings used. Run spx regen hero.png later and it regenerates from that manifest, so you’re never guessing what prompt produced the file sitting in your repo.

--size and --quality are best effort on this backend. It might hand you back a different size than you asked for. If you need an exact pixel size, --exact-size 1200x630 crops and resizes locally after the fact, which needs sharp installed. --transparent generates on a key color and strips it for a real alpha channel, also needs sharp, and refuses to combine with --format jpeg for the obvious reason. --variants 768,1536 writes extra widths beside the main file so you can build a proper srcset without running the generator three times.

The Best Idea In The Tool: Declaring Your Assets

The part of subpixel I’d actually recommend on its own, independent of the rest, is assets.yml. Instead of running spx generate by hand every time a project needs a new image, you declare every image the project needs once:

assets.yml
assets:
- id: hero
prompt: a wide banner, flat vector, teal and slate
out: public/hero.png
size: 1536x1024

Then:

Terminal window
spx sync

spx sync generates whatever is missing or out of date and leaves everything else alone. The part worth building a habit around is spx sync --check. It exits with code 6 if anything is stale, makes zero network calls, and spends nothing. It also reads the artifact bytes back rather than trusting that the recorded inputs still match, which catches the half-finished copy and the image an optimiser rewrote underneath you. That means you can drop it into a CI job as a check that fails the build when someone adds an asset to assets.yml and forgets to generate it, with no API cost and no chance of an accidental image job running on a pull request from a stranger.

Wiring It Into Your Agent

spx init looks at what’s installed on your machine and writes each agent harness the config it actually reads: a Claude Code skill, an MCP server entry for Cursor, Windsurf, Cline, and Kilo Code, and an AGENTS.md snippet for anything with no MCP support at all. Flags are --global, --only <id>, and --dry-run if you want to see the diff before it touches anything. It’s a parse, merge, write operation: any MCP servers you already configured survive untouched, and running it a second time changes nothing.

Claude Code specifically gets the skill and not an MCP entry by default, because it can already run spx straight from its own shell without needing a wrapped tool call. If you want the MCP entry anyway, spx init --only claude-mcp adds it.

For anything else, the manual MCP config is three lines:

mcp.json
{ "mcpServers": { "subpixel": { "command": "npx", "args": ["-y", "subpixel", "mcp"] } } }

spx mcp runs over stdio and exposes seven tools: generate_image, edit_image, sync_assets, list_styles, list_models, get_image_job, and doctor.

Worth checking in any MCP server you hand a filesystem to, this one included: every path an agent supplies is confined to the project directory the server started in, with symlinks resolved. That covers out, out_dir, reference_images, image, and the assets.yml a sync reads. The distinction that matters is who composed the path. A path you type at a shell is a path you chose, so spx generate --out /tmp/x.png writes to /tmp and should. A path arriving through a tool call was assembled by a model out of whatever landed in its context, which might be a scraped page or an issue written by a stranger. Ask the question of anything you wire into an agent, the same way you’d ask whether it needs a shell at all, because a server that takes a destination from a model and passes it straight to a file write will happily write anywhere the process can reach.

One sharp edge to know before your agent hits it: a generation call outlives most MCP host timeouts. If the host asked for progress notifications, it gets them and the call stays open until the image lands. If it didn’t ask, subpixel hands back a job_id after a few seconds and expects the host to poll get_image_job for the result. Poll, don’t retry. A retried call bills you for a whole second generation. It does not check on the first one. Timing runs about 30 seconds on the HTTP backend and up to 6 minutes on the codex-exec backend, both of which are longer than the default timeout on most agent tool calls, so this is the failure mode you’ll actually hit if you skip the doctor check first.

One more expectation to set correctly: spx edit is not in-place pixel editing. It’s a fresh generation guided by the original image as a reference. Details you didn’t mention in the edit instruction can still shift. Treat it as “generate again, but closer to this,” not “change only this one pixel region,” and you won’t be surprised by the result.

Which One Actually Wins

Neither, cleanly. They’re good at different parts of the job.

The GPU wins on privacy, offline use, and volume. Once the hardware exists, generating five hundred images costs the same electricity bill as generating five. Nothing leaves your network, and nothing depends on a vendor’s undocumented endpoint staying up.

The subscription wins on setup time and hardware cost, which for most people building side projects is the number that actually matters: zero. There’s no GPU to buy, no gateway to run, no cold start to wait through. It’s also noticeably better at prompt adherence for anything with text or a specific layout in it, like a diagram with labels or a comparison graphic with two columns, which is exactly the category the self-hosted path struggles with.

As of September 2026, OpenAI’s own pricing page lists Free, Go, Plus, and Pro individual plans, with Plus running about $20 a month. If you’re already paying for that tier for the chat interface, subpixel is using capacity you’ve already bought. Don’t do the math backward from a per-image API price. There isn’t one here, because routing through the subscription is exactly what removes the meter.

My actual routine: subpixel for one-off project images where I want a result in under a minute, and the GPU queue for batch runs where I’m generating a dozen variants of the same illustration and don’t mind the wait. Pick based on how many images you need this week, not on which one sounds more impressive in a README.

Common Questions

Do I need a GPU to generate images for a coding project?

No. A local GPU running ComfyUI or Stable Diffusion is one option, but a subscription-backed tool like subpixel needs no GPU at all. It trades hardware cost for a monthly subscription you may already be paying for the chat interface itself.

Does subpixel work without a ChatGPT subscription?

No. subpixel authenticates through the Codex CLI’s ~/.codex/auth.json, which codex login only writes after you sign in with a real ChatGPT account. There’s no separate free tier or standalone API key path. Without an active ChatGPT login, every command fails at the auth check.

Do I need sharp installed to use subpixel?

No, sharp is an optional peer dependency. Plain spx generate and spx edit work without it. You only need sharp for --exact-size, --transparent, --variants, and the spx icons command, all of which do local image processing after the generation call returns.

Can I use subpixel in CI or on a public-facing site?

Partially. spx sync --check is safe in CI: it makes no network calls, spends nothing, and just exits 6 if assets are stale. Actual generation should not run in CI against untrusted input, and subpixel must never sit behind a public-facing service, since it rides an undocumented endpoint and a personal subscription.

What happens if OpenAI blocks or changes the endpoint?

subpixel stops generating images that day. The endpoint is undocumented, so there is no deprecation window and no support channel. Your assets.yml, your committed images, and their .json manifests all survive, because they are plain files in your repository. Only new generation breaks, which is why declared assets matter.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Pterodactyl vs Pelican vs Crafty
Next Post
Stratis: Red Hat's Answer to Btrfs

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts