Codex Generates Raster Assets: The Repository Problem That Follows
The capability is real. The consequence is a binary file in your repository with no source, no diff and no way to make it again.
A generated PNG is a source file with no source
Codex can produce raster images. Not by describing one for you to make
elsewhere — it calls an image model, gets back pixels, and writes a
.png into a directory. For placeholder art, icon roughs and
banner comps, that closes a loop that used to involve a browser tab and a
download folder.
The interesting consequence is not the picture. It is that a binary file has entered your repository with no upstream artefact, no diff, and no way to produce it again. Every other file an agent writes has a source you can read and a change you can review. This one has neither, and your repository is not set up for that.
What the tool does and where the file lands
Two paths reach the same model. Codex exposes a built-in
image_gen tool that needs no API key of its own, and a
$imagegen skill that wraps it with a documented workflow; the
skill can also fall back to a script driving the API directly when you want
control over size, quality and count. As of April 2026 the model behind it is
gpt-image-2.
The part that surprises people is the destination. Generated images are
written to $CODEX_HOME/generated_images/ — in practice
~/.codex/generated_images/ — not into your working tree.
The skill then instructs the agent to move project-bound assets into the
workspace. That move is an instruction, not a guarantee, which means the
first thing to check after a generation turn is whether the file you think you
have is actually in the repository or sitting in a cache directory outside
it.
#!/usr/bin/env bash
# Anything newer than the last commit that is still in the cache directory
# was generated but never moved into the project.
set -euo pipefail
CACHE="${CODEX_HOME:-$HOME/.codex}/generated_images"
[ -d "$CACHE" ] || exit 0
since=$(git log -1 --format=%cI)
found=$(find "$CACHE" -type f -newermt "$since" -print)
if [ -n "$found" ]; then
echo "generated but not committed to the workspace:" >&2
echo "$found" >&2
exit 1
fi
You cannot review what you cannot diff
Reviewing an agent's work means reading a change. A modified function shows
up as a handful of lines with an argument you can have about them. A generated
PNG shows up as Binary files differ, which is the review
equivalent of a shrug.
This is the same problem as any other unreviewable artefact in a repository, and the same fix applies: make the thing that produced it reviewable instead. For code, that is the diff. For a generated image, the only reviewable object is the prompt.
If your pull-request checks rely on a human reading a diff, a commit that adds twelve PNGs passes review by default. Nobody opened them. Treat any change touching binary assets as needing an explicit visual check, and say so in the template rather than hoping.
Write the prompt down beside the asset
The cheapest useful discipline is a sidecar file. When the agent generates
assets/icons/cart.png, it also writes
assets/icons/cart.png.json containing the prompt, the model, the
date and the size. That file diffs. It is greppable. When someone asks in
November why the cart icon looks different from the rest of the set, the
answer is in the tree rather than in a terminal scrollback nobody kept.
{
"prompt": "flat line icon of a shopping cart, 2px stroke, square corners,
monochrome, no background, centred in a 24x24 frame",
"model": "gpt-image-2",
"generated": "2026-07-03",
"size": "1024x1024",
"postprocess": ["chroma-key transparency", "resize 24x24", "oxipng -o4"],
"status": "placeholder"
}
Put the sidecar requirement in your repository's agent instructions, next to the other conventions the agent is expected to follow. It is the kind of rule an agent obeys reliably because it is mechanical, and the kind a human skips because it feels like paperwork.
Regeneration is not reproduction
Here is where the instinct to treat generated images as build output goes wrong. A build output is something you can delete and recreate. Run the same prompt through the same model twice and you get two different images. Similar, plausibly interchangeable, not identical. So the asset in your repository is not a cache of a derivable thing; it is the thing.
Which means: commit the file, do not gitignore it, and do not put image generation in your build. A CI job that regenerates icons on every run produces a site whose visual identity drifts weekly, and a diff nobody can account for. This is a narrower version of the argument in build system integration for agents — the build should be the deterministic part, and anything that is not deterministic belongs upstream of it, in the repository.
The cost line nobody budgets for
Image turns are not priced like text turns. Reported figures put generation at roughly three to five times the usage-limit consumption of an equivalent text-only turn, with per-image API pricing spanning about two orders of magnitude depending on resolution and quality. Rate limits apply separately, and new accounts start far below the standard ceiling.
The practical effect is that "generate the whole icon set" is a qualitatively different request from "rename these variables", and it will be the turn that ends your session. Generate in small batches, keep what works, and do not loop an agent on visual refinement without a stopping condition.
Transparency is a workaround, not a feature
gpt-image-2 does not produce transparent backgrounds natively. The documented approach is a two-step process using a chroma key: generate against a flat colour, then knock that colour out. It works well enough for prototyping and badly enough that you will see fringing on any asset placed over a background it was not keyed for.
Chroma-keyed edges look clean against the background you tested and grubby against a dark theme. If your product has a dark mode, check every keyed asset in it before deciding the pipeline works.
For anything with a hard edge and a flat fill — which is most UI iconography — a vector drawn in code sidesteps the problem entirely, and that trade-off is the subject of the companion piece on drawing in SVG.
Where generated raster assets actually belong
They are good at the thing they are good at: getting a screen to look finished before anyone has decided what it should look like. A dashboard mock with real-looking empty states reads as a design; the same screen with grey boxes reads as an argument about grey boxes. That is genuine value during build-out.
They are poor as final assets, for reasons that have nothing to do with quality. You cannot restyle them when the brand changes. You cannot ask them to match a stroke weight. You have a licensing question you probably have not asked. And they carry no source, so the next person to touch them regenerates rather than edits, and the set drifts.
Mark them as placeholders in the sidecar, and gate on that marker in CI, so the decision to ship one is made deliberately rather than by forgetting.
Binaries in git, and the moment that stops being fine
Git stores every version of every file forever. For text that is inexpensive, because a diff is small. For a 900KB PNG regenerated eleven times during a design conversation, you have permanently added ten megabytes to every clone, including the ten versions nobody wants.
A repository with a handful of generated assets does not care. A repository where an agent has been iterating on imagery for three months does, and by then the history is written. The usual answer is Git LFS, which stores pointers in the tree and objects on a server, and which introduces its own friction: contributors need the extension installed, some CI images do not have it, and a shallow clone without LFS gives you pointer files where you expected pictures.
My preference is to avoid the question by keeping the count low. Generate into a scratch directory, pick one, commit that one. The discipline that keeps generated assets reviewable — a sidecar, a marker, an explicit decision — also keeps the count low, because you cannot casually commit twelve variations of a thing that requires a manifest entry each.
A pre-commit hook that rejects any added binary over a threshold — say 250KB — unless it carries a sidecar is a two-line script and catches the entire class. Raw model output tends to arrive at 1024×1024 whether or not you needed it at 24 pixels.
Reviewing a commit that adds images
The diff is useless, so the review has to be a checklist. Six questions, none of which require opening a design tool:
Is every added binary accompanied by a sidecar naming the model, prompt and date? Is each marked as placeholder or final, deliberately? Is the resolution proportionate to the use — a 24-pixel icon should not be a megabyte? Has the asset been through the optimiser? Does anything under production source reference a placeholder path? And has one human actually looked at the images, in both themes?
That last one is the only step that costs attention, and it is the one that gets skipped, which is why the other five are worth automating. This is the same argument as reviewing agent-authored code: reserve human attention for the judgement that only a human can make, and mechanise everything around it so the judgement actually gets made.
Retrofitting provenance onto assets already in main
Most teams arrive at this article after the fact, with a directory of images of uncertain origin. You cannot recover a prompt that was never recorded, and pretending otherwise produces a manifest full of confident fiction.
What you can do is establish the boundary. Write a manifest that marks
every existing asset as "provenance": "unknown, pre-policy" with
the date the policy started, and require full entries for everything added
after it. That is honest, it is greppable, and it converts an
unbounded problem into a finite list someone can work through when the brand
refresh forces the question anyway.
Then decide, once, what happens to the unknown pile. Usually the answer is that internal-facing assets stay and anything customer-facing gets regenerated or replaced deliberately before the next release that touches it.
The prompt is the asset, so version it like one
Once you are writing prompts into sidecars, a second problem appears: the prompts drift. The cart icon was generated in July with a description of your style; the bell icon in September with a slightly different one, because whoever ran it wrote the prompt from memory. The two icons do not match, and the sidecars record faithfully that they were asked to be different things.
Treat the shared part of the prompt as a file. One string, checked in, that every generation turn prefixes, describing the house style once: stroke weight, corner treatment, palette, framing, background. The per-asset prompt then describes only the subject, which is the only thing that should vary.
House style for all generated iconography. Prefix every icon prompt
with this block verbatim; do not paraphrase it.
Flat two-dimensional line art. Uniform 2px stroke at 24px nominal size,
no fills, no gradients, no shadows. Square line caps, 2px corner radius.
Single subject centred in a square frame with 2px optical padding.
Pure white background, no scene, no perspective, no text.
The instruction not to paraphrase matters more than it looks. An agent asked to include a style description will helpfully improve it, and a helpfully improved style description is a different style. Pin the string, and when the style genuinely changes, change the file in a commit that also regenerates the set.
This is the same discipline as any other shared constant, and it fails the same way when it is prose living in someone's head. The general case — what belongs in a repository's instruction files, and why duplicating it makes agents worse rather than better — is the argument in README duplication degrades agents. A style prompt is that argument applied to pixels.
What I actually do
Generated assets are allowed, in one directory, with a sidecar each, marked as placeholders by default. The style prefix is a file. The pre-commit hook rejects binaries over 250KB without a sidecar. CI fails if production source references the placeholder path, and prints the outstanding count on every build. Nothing regenerates in CI, ever.
That setup took an afternoon and it has never once needed maintenance, which is the property I care about. The alternative — a policy in a wiki, and reviewers who are supposed to notice — needs maintenance constantly and works about half the time, because noticing a PNG in a forty-file diff is not a thing humans do reliably at five in the afternoon.
Codex generating a PNG is a real capability with a real repository problem attached: the file arrives with no diff, no source and no reproducibility. Write a sidecar with the prompt and model beside every generated asset, commit the image rather than treating it as build output, mark placeholders explicitly and gate on that marker, and budget for image turns separately — they consume usage several times faster than the text work around them.