Generate images from the command line with your ChatGPT/Codex subscription. It needs no API key and doesn't run the codex agent.
codex-img calls the same image endpoint the Codex CLI uses, chatgpt.com/backend-api/codex/images/generations, or /images/edits when you pass reference images. That's one JSON request per image, with no chat model in between. It reuses the ChatGPT login that codex login saved in ~/.codex/auth.json.
It's also a local asset pipeline that needs no login and no quota. It can convert, trim, resize and key out backgrounds (whole folders at once), snap images to palettes, lay a set out on a contact sheet, pack texture atlases for PixiJS or Phaser, and build tile pyramids for big maps. Everything it writes is deterministic: the same inputs give the same bytes. Asset batches join both halves, going from a JSON spec to generated, cleaned and converted files in one pass.
Prebuilt binary: download the archive for your platform from Releases. Builds exist for macOS (Apple Silicon and Intel), Linux (x86-64 and ARM64, fully static) and Windows (x86-64, a .zip). Unpack it and put codex-img on your PATH. The archive also contains the agent skill in skills/codex-img.
tar -xzf codex-img-*-aarch64-apple-darwin.tar.gz
install codex-img-*/codex-img ~/.local/bin/On macOS, a binary downloaded with a browser may be quarantined; clear that with xattr -d com.apple.quarantine ~/.local/bin/codex-img.
On Windows (PowerShell):
Expand-Archive codex-img-*-x86_64-pc-windows-msvc.zip -DestinationPath $env:LOCALAPPDATA\codex-img
# then add the unpacked codex-img-* folder to your PATH, or run codex-img.exe from itThe login is read from %USERPROFILE%\.codex\auth.json (or %CODEX_HOME%\auth.json), where codex login puts it.
From source:
./scripts/install.sh # needs a Rust toolchain (https://rustup.rs)On Windows, install.sh doesn't apply: run cargo build --release and use target\release\codex-img.exe.
This builds target/release/codex-img, a single ~2 MB binary with no runtime dependencies, and symlinks:
- the binary to
~/.local/bin/codex-img(override withBIN_DIR); - the agent skill
skills/codex-imginto~/.claude/skills/and~/.codex/skills/, for whichever of those tools you have installed.
You need to be logged in once with codex login (ChatGPT sign-in). codex-img status checks the login offline, without using any quota.
skills/codex-img/SKILL.md is an Agent Skill. Claude Code, Codex and other skill-aware agents load it automatically when a task involves making or editing an image. It covers the commands to run, when to spend quota, how to handle each exit code (don't retry auth or quota errors), and prompt-writing tips. It stays short and points to references that an agent opens only when a task needs them:
prompting.md: prompt recipesgame-assets.md: sprites, views, characters, palettes,batch,sheetandtileprojects.md: project folders, run history and manifestsconvert.md: local conversion and background removalfallback.md: the Python script's limits
When the binary isn't installed, the skill falls back to skills/codex-img/scripts/codex_img.py. It's a standard-library-only Python 3.9+ script with the same flags, exit codes and --json output, but it only covers the direct route and only writes PNG: there's no JPEG/WebP conversion, no quantization and no --via-responses. Agents can convert the output themselves. That makes the skill folder usable on its own, just by copying it into ~/.claude/skills/ or ~/.codex/skills/.
codex-img "a red fox in snow, flat vector" # -> ./codex-img-<time>-<id>.png
codex-img "app icon, paper plane" -o icon.webp
codex-img "a lighthouse on a cliff at dusk" -a 16:9 # frame shape, see below
codex-img "make it night with aurora" -i fox.png -o fox-night.jpg
codex-img "sticker of a cat" --background transparent -n 4 -o stickers/
codex-img "flat app icon, paper plane" -c 64 -o icon.png # palette PNG, ~10-70x smaller
codex-img "race car sprite" -b transparent --trim=4 --resize 400x -o car.png # + car.raw.png
echo "long prompt..." | codex-img - --json
codex-img status --json # login check, uses no quota| Option | |
|---|---|
-o, --output |
File or directory. A trailing / or an existing directory gets generated names. With -n, files get -1, -2, … suffixes. Existing files are never overwritten: a taken name is refused before any quota is spent. |
-i, --image |
Reference image to edit or compose (PNG/JPEG/WebP, repeatable, max 5) |
-f, --format |
png | jpeg | webp. Defaults to the -o extension, then png. The endpoint returns PNG and the rest is converted locally: jpeg with transparency flattened onto white; webp lossy (libwebp), keeping transparency |
--output-quality |
1–100 for jpeg (default 90) and lossy webp (default 80). Separate from -q, which is only a hint to the backend |
--lossless |
Lossless webp with every pixel kept exactly. Much bigger than lossy |
-c, --colors |
Quantize PNG output to a palette of 2–256 colours, like pngquant. Transparency is kept. Best for flat art: a 969 KB icon came out at 13 KB with 64 colours |
--dither |
Dither while quantizing. Smooths gradients and photos, but makes files larger |
-a, --aspect |
W:H from 1:3 to 3:1, such as 16:9, 2:3 or 1:1. Starts the prompt with "The frame must be in 16:9 landscape format, wider than it is tall.", which is what sets the shape (see the backend), and warns on stderr if the image comes back more than 2% off. Can't be combined with --size. For exact pixels, add --resize WxH --fit cover |
-s, --size |
WxH or auto. Sent as the request's size, which the backend has ignored in tests; use --aspect |
--view |
Camera preset: side, front, top-down, three-quarter, isometric, or one of your own. See Presets |
--style, --character |
Named style and character presets (repeatable): their text goes into the prompt, their images are sent as labelled references |
--style-ref, --character-ref, --composition-ref |
Reference images with a role (repeatable). They're sent after the -i images, max 5 images in all, and each gets a line in the prompt saying how to use it |
--palette, --palette-clean |
Limit the image to a palette: hex codes, a .gpl/.hex file, a swatch image, or a palette preset (14 built in, such as pico-8, nes or resurrect-64, or your own). See Palettes |
--parent, --no-parent |
Set a version parent explicitly, or suppress the automatic link from a single -i reference. |
--manifest |
Writes <image>.json: the prompt sent, presets, inputs (with fingerprints) and what the backend reported. Automatic inside a project |
-q, --quality |
low | medium | high | auto |
-b, --background |
transparent | opaque | auto |
--via-responses |
Fallback route: a routing model calls the image_generation tool through the Responses API. The prompt may be rewritten and --size is ignored |
-m, --model |
Routing model for --via-responses (default gpt-5.5) |
--trim, --resize, --fit, --hard-alpha, --no-enlarge, --nearest, --no-bleed |
Trim, resize, hard alpha and edge bleed before saving, as in convert. With --trim, --resize or --hard-alpha the untouched original is also kept as <name>.raw.<ext>, since quota was spent on it and there's no seed to regenerate it |
-n, --count |
Images to generate in parallel (1–10). Each one is a separate request |
--json |
Prints one JSON line per image: path, size, submittedPrompt (when presets, labels or --aspect changed the prompt), quality (the backend's), outputQuality (the JPEG/WebP quality codex-img applied, when it encoded lossily), revised prompt, usage, duration. After --trim, --resize or --hard-alpha: size is the saved file's, plus rawSize (the backend's), rawPath and trim |
--quiet |
No progress output on stderr |
PNG output is always recompressed losslessly with oxipng. That roughly halves the backend's PNGs (946 KB → 462 KB in testing) without changing a visible pixel. The only pixels that change are fully transparent ones, which get the nearest edge colour (see edge bleed under Converting existing images; --no-bleed turns it off).
Which format to pick, from one generated 1536x1024 image (946 KB from the backend):
| Output | Size | Good for |
|---|---|---|
webp (lossy, q80) |
7.5 KB | Web images, gradients, photos |
png -c 64 |
8 KB | Flat art, icons, stickers; keeps transparency |
jpeg (q90) |
82 KB | Places that don't take WebP |
png |
462 KB | Exact pixels, further editing |
webp --lossless |
469 KB | Exact pixels, in WebP |
Paths go to stdout and progress goes to stderr, so the tool composes well in scripts and agent tools.
Exit codes: 0 ok, 1 error, 2 auth, 3 quota, 4 moderation, 64 usage.
Presets name the parts of a prompt you'd otherwise repeat and let drift:
- Views (camera):
side,front,top-down,three-quarterandisometricare built in, and you can define your own. - Styles: text and, optionally, style reference images.
- Characters: text and identity reference images, so one character keeps its looks across scenes.
codex-img "Game asset sprite: an old dockside crane. Isolated, no ground." --view side -b transparent -o crane.png
codex-img presets add character captain --ref art/captain.png \
--text "a stocky walrus sea captain with big tusks, in a yellow raincoat and a white captain's hat"
codex-img "The captain steering a ship's wheel in a storm." --character captain -a 3:2 -o storm.png
codex-img "A fox." --style-ref art/style.png -a 1:1 -o fox.png # a reference with a role, no preset
codex-img presets # what's defined, and whereThe prompt that's sent is put together in this order, and --json shows it as submittedPrompt:
- The
--aspectsentence. Camera:followed by the view's text.- One line per reference image.
-iimages come first and keep their numbers, so "Image 1" in your prompt still means the first-i.Image 2: style reference only: take its palette, rendering and line work, not its subject or layout.Image 3: character reference for "captain": keep the same character (face, proportions, outfit, colours) in a new pose and scene.Image 4: composition reference only: follow its layout and framing, not its subject or style.
The character "captain": <its text>.- Your prompt, then each style's text.
Where presets live. The first of these that defines a name wins:
- a batch spec's own
views,stylesandcharacters - the project: the nearest
codex-img.jsonin the current folder or one above it - global:
$XDG_CONFIG_HOME/codex-img/presets.json, else~/.config/codex-img/presets.json - built in: the views, and the palettes listed under Palettes
Preset files are only read when a preset is named, so a broken one can't stop a plain run. codex-img presets lists every preset with its source, and marks the ones another layer hides.
{
"views": {"roadside": "seen straight on from the side at eye level, its bottom edge a straight line"},
"styles": {"harbour": {"text": "16-bit pixel art, bold colours", "refs": ["refs/boat.png"]}},
"characters": {"captain": {"text": "a stocky walrus sea captain", "refs": ["art/captain.png"]}}
}A string is short for {"text": ...}, and ref paths are relative to the file. Edit the file by hand, or use:
| Command | |
|---|---|
presets add <kind> <name> [--text T] [--ref IMG]... [--global] [--force] |
Writes to the project's codex-img.json (created in the current folder if there's none), or the global file. A ref inside the project is stored as a relative path; one outside it is copied to presets/<kind>/<name>/. Global refs are always copied, to refs/<kind>/<name>/ in the global folder. --from IMG is the same as --ref IMG |
presets show <kind> <name> |
Text, refs (and whether they exist) and the file it's defined in |
presets remove <kind> <name> [--global] |
Removes the entry, and the refs folder add made. Refs inside the project are never deleted |
presets promote <kind> <name> [--force] |
Copies a project preset, refs included, to the global file |
Give a character or style only the text that defines it. The text goes into every prompt that uses it, so a pose or "isolated on a transparent background" would end up in every scene too (see the backend).
The Python fallback has the built-in views and the --*-ref options, but not named styles, characters, palettes or your own views.
codex-img rerun art/fox.png -o art/fox-again.png --json
codex-img rerun art/fox.png -n 3 -o art/variations/ --jsonRerun reads <image>.json and submits the recorded prompt, references and request settings,
then applies the recorded conversion. Preset edits do not change this request. It costs one
image per result (1 by default, up to 10); the backend has no seed, so the result varies.
Existing files are preserved. The new manifest and events link to the original with parent.
Ordinary edits with one -i also get a parent; use --no-parent to suppress it or
--parent <image> to set a different one.
A changed reference stops before quota. --anyway permits changed references and warns
which ones it uses. Missing references always stop; edit the request to omit one. Older
manifests replay available settings with a warning about default conversion, without needing
--anyway. New manifests record conversion settings and fingerprints of the bytes sent.
Tile reruns repeat the seam repair using the original panorama.
presets list --json is free: one JSON line per preset with kind, name, text, absolute
refs, and source. Palettes include hex colors; hiddenBy marks overridden entries.
codex-img batch art/assets.json --inspect --json # status, conversion and history; free
codex-img batch art/assets.json --reroll hero --dry-run --json
codex-img batch art/assets.json --reroll hero # costs one image per planned generation
codex-img batch art/assets.json --restore hero art/chosen.webp # free; keeps the source
codex-img refine --batch art/assets.json --key hero --from-commentRe-roll accepts exact keys after the spec, either a list or repeated --reroll. Missing
references are included in the dry-run cost. The current raw stays active until the new PNG
and manifest are ready; then the old files move to .codex-img/history/<key>/. The new raw
links to the history file as its parent. Generation failures preserve the current raw. If the
raw or manifest changes during generation, activation stops and keeps the generated version
in history for recovery.
Restore copies any chosen image into the raw slot, preserving the source and archiving
the current raw. A source manifest is copied except for its comment and star. It converts JPEG/WebP sources to PNG and sets the new
raw’s parent to the selected source. Conversion then uses the current batch settings. Outside
a project, history lives in .codex-img/history/ beside the spec. --inspect --json lists each
asset’s raw/output paths, missing/changed state, comment, star, conversion and history without
login or quota. Pending generated files that were not activated are excluded from history.
Restore accepts PNG, JPEG and WebP sources, including images without a manifest. Such sources get a minimal raw manifest with parent and the project root where applicable. Restore preserves existing manifest fields without inserting missing inputs or setup. Source comments and stars stay on the source and are not copied. If the current raw manifest has a non-empty comment, restore stops before changing images; clear or move that comment first (batch comments belong in the spec).
Batch --inspect --json includes an edited boolean. For refined raws, changed compares the spec with the first generation manifest in the parent chain. Missing history gives changed: false, edited: true. Comments still do not count as changes.
Batch generation, re-roll, restore and refine --batch lock each spec separately by its canonical path under .codex-img/locks/. Different specs can run concurrently. If the same spec is busy, stderr says “Waiting for another batch run on …” before waiting. Add --no-wait to fail immediately instead (also supported by refine --batch). --convert-only takes one spec lock for the conversion phase, so its workers run in parallel and --no-wait fails once before any conversion if the spec is busy. Dry runs and inspection do not lock. An unused legacy .codex-img/batch.lock is removed when a new spec lock is acquired; a legacy lock held by an older process is left in place.
After reviewing a batch dry-run plan, use --expect-images N to stop before quota if the
number of generations (including missing dependencies) changed. The CLI enforces this count
when the run starts, so a cost reviewed from the dry run still applies.
convert --json also reports hardAlphaPixels (pixels made transparent by hard alpha,
before and after resizing), keyedOutPixels, and paletteColors (the selected palette size).
Its existing trim rectangle is in input coordinates. --mask-out mask.png with --key
writes an input-sized grayscale PNG: white marks pixels removed by keying, black marks the
rest. The mask stays in input coordinates even when the output is trimmed or resized. It
needs one input; its path must differ from the input and output. All of this is local and free.
codex-img check codex-img.json --json
codex-img check art/assets.json --jsonCheck validates presets and batch specs with the same parsers used to run them. It needs no
login and uses no quota. JSON output is a list of {path, message} errors, or [] when
valid. Exit code 0 means valid; 64 means invalid. Independent assets and preset entries are
checked even when another entry is broken. --kind=batch|presets selects the type explicitly, including for an editor’s temporary copy. Otherwise the filename and fields identify the type.
The editor schemas are in schemas/batch.schema.json and schemas/codex-img.schema.json.
Point a file’s $schema at the matching schema to enable autocomplete in your editor.
The CLI accepts this field and never downloads or executes its value. The CLI also checks
relations that a schema cannot express, such as missing presets and reference cycles.
A comment is one plain comment string: on a batch asset in its spec, or in an image's
<image>.json manifest. Stars are "star": true in manifests. These fields never change
the generation request or the batch's changed check.
codex-img comments --json # comments in this project; no quota
codex-img stars --json # starred images; no quota
codex-img comments art/fox.png --set "Make the hat blue"
codex-img comments --batch art/assets.json --key hero --set "Blue hat"
codex-img stars art/fox.png --set trueGive a folder to scan elsewhere. Listings print one JSON object per line with paths relative
to the scanned folder: image comments have image and comment; batch comments have spec,
key, rawPath and comment; stars have image and star. The scan skips .git/,
.codex-img/, node_modules/, target/ and symlink directories. Empty comments are omitted.
Broken image manifests and recognizable batch specs are skipped with warnings on stderr;
unrelated JSON/JSONC files are ignored quietly. Specs are recognized by a top-level assets
key, including when its value is incomplete; stars scans only image manifests. JSON stdout
stays NDJSON and a completed listing exits 0.
Use --set "" to clear a comment, or --set false to remove a star. Changes preserve the
other JSON fields and use a lock and an atomic replacement. --expect "old text" (or
--expect true|false for a star) stops if someone changed that field since you read it.
These review commands need no login and spend no quota. Outside projects, review locks live
beside the global event log; reviewing an image does not create a local .codex-img/ folder.
codex-img refine art/fox.png "Make the hat blue" -o art/ --json
codex-img refine art/fox.png --from-comment -o art/ --jsonRefine costs one image. It sends “Change only: . Keep everything else exactly as it
is.” with the saved image as its content reference. It keeps the recorded request and local
conversion settings, creates new files, and sets parent to the original. Named presets
use their current project/global definitions. It compares the original submitted prompt and
input fingerprints with today's composition and warns with preset names when text,
reference selection or reference contents changed. Combined text can be ambiguous without
snapshots; the warning says when a change cannot be attributed to one definition. A missing
recorded preset or current reference stops before generation. Preset definitions are not
snapshotted in manifests. When a single preset’s old text can be recovered from the submitted
prompt, the drift warning includes short old and new excerpts.
New manifests made from a parent record kind: "rerun" for rerun and batch re-rolls,
"edit" for refine and batch edits. The same field is on the job.started event.
Comment and star changes edit only that field's text in the manifest or spec, so a hand-written batch spec keeps its layout.
--from-comment takes the edit from the image's comment. The new manifest records it as
fromComment; the original comment is cleared only after the image and new manifest are
saved. Errors preserve it. A comment changed during the run is also preserved, with a
warning. --expect-comment "old text" stops if it changed before starting. Batch refinement
uses refine --batch <spec> --key <key> "<change>", or --from-comment. It keeps
the raw’s recorded generation settings and preset names (current definitions, with drift
warnings), then applies the batch’s current conversion settings. Use re-roll to adopt changed
spec generation settings.
codex-img "a treasure chest sprite" --palette '#2B1D14,#6B3E26,#C7743A,#F2C14E,#F7EBD0,#3B6E5A' -b transparent -o chest.png
codex-img "a treasure chest sprite" --palette game-boy -b transparent --trim --resize 400x -o chest.png
codex-img convert old-art/*.png --palette pico-8 --palette-clean -o snapped/ # art made without it
codex-img presets add palette harbour --from lospec-swatch.png # or --text '#... #...'--palette does two things:
- On generation, it adds the hex codes to the prompt ("Use only these 6 colours, exactly, and no others: #2B1D14, …").
- After generation, and in
convert, it snaps every colour to the nearest palette colour, so the file has exactly those colours:- Alpha is hardened at 127 unless
--hard-alphasays otherwise. - Stray pixels are cleaned at full size.
- Colours are snapped again after
--resize, which blends neighbours. - PNG output is a palette PNG. WebP needs
--lossless, and JPEG and-care refused.
- Alpha is hardened at 127 unless
The untouched original is kept as <name>.raw.png.
A palette can be:
- hex codes (commas or spaces,
#optional) - a GIMP
.gplfile - a
.hextext file (as Lospec exports it) - a swatch image (its distinct colours)
- the name of a palette preset: a built-in one (below), or one under
"palettes"incodex-img.json, global presets or a batch spec
| Built-in palette | Colours | |
|---|---|---|
pico-8 |
16 | PICO-8 fantasy console |
game-boy |
4 | original Game Boy greens (#0F380F to #9BBC0F) |
nes |
55 | NES, as Lospec lists it (NES palettes differ by emulator) |
c64 |
16 | Commodore 64, as Lospec lists it (one of several measured versions) |
zx-spectrum |
15 | ZX Spectrum, normal and bright |
cga |
4 | CGA palette 1, high intensity: black, magenta, cyan, white |
ega |
16 | EGA's default 16 colours (the RGBI set, with brown #AA5500) |
ega-64 |
64 | every colour EGA could show (2 bits per channel) |
sweetie-16 |
16 | by GrafxKid |
dawnbringer-16, dawnbringer-32 |
16, 32 | by DawnBringer |
endesga-32 |
32 | by ENDESGA |
resurrect-64 |
64 | by Kerrie Lake |
aap-64 |
64 | by Adigun A. Polack |
The values come from Lospec. Each can be overridden by defining a palette of the same name. There's no Game Boy Color palette, because that console had no fixed one.
In a batch spec, use the palette and palette_clean fields.
--palette-clean is for art that wasn't generated with the palette. It reduces the image to 32 colours, matches hue before lightness, then despeckles. Without it, shading that falls between palette colours flickers between them as speckles and streaks of another hue. It changes images that were prompted with the palette more than it needs to (pixel art especially), so it's off by default.
codex-img convert runs the same local pipeline on files you already have. It needs no login and uses no quota. It reads PNG, JPEG, WebP and GIF, and writes PNG, JPEG or WebP. A GIF becomes PNG unless -f or -o says otherwise.
codex-img convert hero.png -o hero.webp # PNG/JPEG/WebP in, any of them out (lossy WebP)
codex-img convert icon.png -c 64 # -> icon.min.png, palette PNG
codex-img convert photo.png # -> photo.min.png, lossless recompression only
codex-img convert shots/*.png -f jpeg -o out/ # several inputs need a directory
codex-img convert car.png --trim=4 --resize 400x -o sprites/ # crop to content, then resize
codex-img convert cockpit.png --resize 1920x1080 --fit cover # exactly 1920x1080, centre cropWith -r/--recursive, an input can be a directory. Every PNG, JPEG, WebP and GIF under it is converted in sorted path order and goes to the same relative path under -o (or next to itself without -o). Hidden files, symlinked directories and non-image files are skipped. Images in formats codex-img can't read (BMP, TIFF and so on) fail on their own line, and the exit code reports the failure. If two inputs would write the same output (a.png and a.jpg with -f webp), the run stops before converting anything. With --json, a last line {"total":{"files","failed","inputBytes","bytes"}} follows the per-file lines, which already have inputBytes and bytes.
codex-img convert -r maps/units/ -f webp --lossless -o web/units/ --jsonWithout -o, output goes next to the input as <name>.<ext>, or <name>.min.<ext> when that would be the input itself. It takes -o, -f, -c, --dither, --output-quality, --lossless, --json and --quiet, with the same meaning as above, plus:
| Option | |
|---|---|
--trim[=PAD] |
Crop transparent borders to the visible pixels (alpha above 16: generated images scatter fainter specks over the background, and those don't count), keeping PAD transparent pixels around them. --json reports the crop as trim: {x, y, width, height} in input pixels, so sprites can keep their anchor. An image with no transparent border (any opaque image, every JPEG) is left as it is, padding included |
--resize SIZE |
WxH, Wx or xH, up to 8192 per side. A missing side keeps the aspect ratio. Runs after --trim |
--fit MODE |
For WxH with another aspect ratio: inside (default; fits in the box, so one side may be smaller), cover (exactly WxH, crops the centre), contain (exactly WxH, pads with transparency, or white in JPEG), fill (stretches) |
--hard-alpha[=N] |
Make every pixel fully solid (alpha above N, default 16) or fully transparent. Runs before --trim, and again at 50% after --resize so resampled edges stay hard. For pixel art and other crisp sprites. It also fixes the backend's "solid" pixels, which come back at alpha 250–254 |
--nearest |
Copy source pixels instead of smoothing, with --resize. For whole-number upscaling of pixel art |
--no-enlarge |
Only shrink: the scale never goes above 1, so --resize leaves smaller images at their size. cover still crops to the box's aspect ratio, contain pads the unscaled image, fill caps each side on its own |
--key COLOUR |
Make an unwanted background transparent, such as the sea painted under a boat or the sky behind a building: pixels of this colour that connect to the transparent background or the image's border. It's a flood fill, so matching paint enclosed by the object's outline (a blue stripe on a hull) survives, and small islands the removed background leaves behind, like foam and spray, go with it. COLOUR is auto[:TOL], a name (red, orange for browns too, yellow, green, cyan, blue, purple, pink, white, gray, black), or #rrggbb[:TOL]. auto uses the background's own colours, sampled from the outer 5% along the --key-region edges, or all four edges without a region. TOL is per channel, default 32. Repeatable. Runs after --hard-alpha and before --trim |
--key-region BANDS |
Only key out within bands along edges of the visible content, each a share of its height or width: bottom:30% (ground under a sprite), top:80% (sky behind a building), top:40%,left:15%, or all:20%. Matching colours elsewhere, like a sky-blue window, stay safe |
--key-spread STEP |
Let keying spread from each removed pixel into neighbours whose colour differs by at most STEP per channel (e.g. 16), step by step. It then follows gradients the key colours don't cover, like a sky fading from blue to gold, and stops at outlines. Higher steps also take low-contrast scenery, like distant hills |
--key-cut[=F] |
Before --key, from each --key-region edge inward, cut off whole rows (or columns) while at least F (default 0.4) of their visible pixels match the key: below a boat's waterline, for example. Only for objects that span most of the line; a narrow building in a wide sky would be cut through |
--trim-density [EDGES:]F |
With --trim, also drop sparse rows at the bottom: those with fewer visible pixels than F (e.g. 0.15) of the fullest row. Leftover specks under a sprite then don't become its bottom edge, which would make a sprite that stands on its bottom edge float. Other edges: top:0.15, bottom,left:0.15, all:0.15. A thin mast, pole or trunk is sparse too, so check the result |
--no-bleed |
Keep the colour stored under fully transparent pixels (see below) |
--effort N |
WebP encoder effort, 0 (fastest) to 9 (smallest), like cwebp -z (default 6). At 9, lossless WebP is about 5–25% smaller and takes several times longer. An explicit effort re-encodes lossless WebP input (lossy input is never re-encoded for it) |
--force |
Replace existing output files (never an input). The file is written to a temporary name and renamed into place, and one that already holds the same bytes is left untouched (--json reports unchanged: true) |
Use --nearest for whole-number upscaling of pixel art, such as a 32×32 sprite to 96×96. Downscaling generated pixel art with it gives uneven pixels, because the art is not on a true grid. Grid detection is a later feature. Batch assets and defaults accept nearest: true; it is a flag, with no filter selection.
Saved conversions appear in the run feed, linked to the source. Temporary previews can use CODEX_IMG_EVENTS=off.
Generated images with a transparent background often store a dark vignette under the transparent pixels. It's invisible until something resizes or filters the image without premultiplying alpha, and then it shows up as a dark halo around the edges. convert and generation deal with it twice. Its own resize uses premultiplied alpha, so the hidden colour never reaches the result. For PNG and lossless WebP output, it also writes the nearest visible colour under every fully transparent pixel (edge bleed, as texture tools do), so a game engine's texture filtering blends toward the edge colour. Visible pixels and alpha don't change. As a side effect, the file usually gets much smaller, because the noisy hidden colours are gone. Lossy WebP replaces those colours on its own, JPEG has no alpha, and palette PNGs (-c) are left alone so the bled colours don't use up palette entries. --no-bleed keeps the stored colours as they are; use it with --lossless when every pixel must be kept exactly, including invisible ones. The --json output also has inputSize. Animated WebP, PNG (APNG) and GIF are refused rather than converted, because only the first frame would survive. A GIF whose frames are all the same counts as a still image. A file that already matches the request (a lossy WebP converted to WebP with default settings, for example) is copied rather than re-encoded, so it doesn't lose quality again. Without --force, existing files are never overwritten, and the input never is. The same input and options always give the same bytes, -c included, so re-running a pipeline with --force only rewrites files whose output really changed. Each input is converted independently: if one fails, the others still run and the exit code reports the failure.
The image model sometimes paints a background despite "no ground" or "transparent background" in the prompt. Most often it's a patch of sea under boats and docks; for pictures with an opaque background, it's the whole sky. --key removes such a background:
# Sea painted under a boat: sample along the bottom, cut below the waterline, stop specks making it float.
codex-img convert boat.png --hard-alpha --key auto --key-region bottom:30% --key-cut --trim --trim-density 0.15 -o sprites/
# Sky behind a building: sample along the top, follow the gradient, add white for clouds with outlines.
codex-img convert barn.png --key auto --key white --key-region top:80% --key-spread 16 --trim -o sprites/--key auto can't tell the background from the object's own edge. On a sprite without painted ground, the bottom rows are a trunk, wheels or a pole, and sampling there would key them away. So point the region at edges that really are background. When auto picks up too much or too little, name the colours instead (--key blue --key white) or change --key-spread. Scenery with nearly the sky's colour, like hazy distant mountains, goes with the sky; use a shallower --key-region to keep it. Check a batch on a contact sheet (codex-img sheet).
A bare convert as the first argument always runs this subcommand. A prompt that starts with the word still works when it's quoted, e.g. codex-img "convert this sketch into a watercolor" -i sketch.png.
codex-img sheet lays images out in one labelled grid, so a batch can be reviewed as a single image. That's cheaper for an agent than opening each file, and it's how problems across a set show up. Sprites stand on a line at the bottom of their cell, so one with empty rows or leftover specks under it visibly floats. Like convert, it needs no login and no quota. Saved sheets appear in the run feed; previews can use CODEX_IMG_EVENTS=off to stay out of history.
codex-img sheet public/assets/harbor/*.png -o harbor-sheet.png
codex-img sheet sprites/*.png -o sheet.png --same-scale --cols 8 --force| Option | |
|---|---|
-o FILE |
The sheet (required): .png, .jpg or .webp |
--cols N |
Columns (default: a roughly square grid) |
--cell PX |
Cell size, 32–2048 (default 240) |
--bg COLOUR |
Background as #rrggbb (default #96be96, a muted green that neither white nor dark sprites blend into) |
--labels WHAT |
name (default), path (as given on the command line) or none |
--center |
Centre sprites in their cells instead of standing them on the baseline |
--same-scale |
Shrink every sprite by the same factor, so their sizes stay comparable. By default each shrinks to fit its own cell. Sprites are never enlarged |
--force |
Replace an existing sheet |
An input that can't be read is reported and left out, and the others are still laid out. Labels use a small built-in font that covers ASCII; other characters show as ?.
codex-img atlas packs many small images into texture atlas pages for a game engine. Each page gets a JSON file in TexturePacker's "hash" format, which PixiJS, Phaser and most engines load. It needs no login and no quota, and the same inputs and options always give the same bytes, so builds are reproducible.
codex-img atlas maps/units/ -o web/units.webp --prefix units/ --lossless --trim --extrude 1
codex-img atlas flags/ -o web/flags.png --max-size 1024 --pot --forceImages in a directory are found recursively and named by their path under it, without the extension (Germans/infantry). A file given directly is named by its file name. When one page isn't enough, the next pages are units-1.webp + units-1.json and so on. The first JSON lists the others in meta.related_multi_packs, so PixiJS's Assets.load('units.json') loads every page. Inputs can be PNG, JPEG, WebP or GIF. Sprites are never rotated. Images with the same visible pixels (after --trim) are stored once, and all their frames point at the same place on the page, each keeping its own spriteSourceSize and sourceSize; the colour under fully transparent pixels doesn't count. The last page is sized to fit, as close to square as packing allows. Non-image files in a directory are skipped with a warning, but an image in a format codex-img can't read (BMP, TIFF and so on), an unreadable input or a duplicate frame name fails the whole atlas rather than leaving a frame out.
| Option | |
|---|---|
-o FILE |
First page (required): .png or .webp |
--prefix TEXT |
Put this in front of every frame name, e.g. units/, so frames from different atlases don't collide in a global texture cache |
--trim |
Pack only the pixels with any alpha. spriteSourceSize and sourceSize keep the original offset and size, so sprites stay aligned |
--padding PX |
Transparent space between sprites and at the page edges, 0–64 (default 2) |
--extrude PX |
Repeat each sprite's edge pixels this far outward, outside its frame, so texture filtering doesn't pick up neighbours, 0–16 (default 0) |
--max-size PX |
Largest page side (default 2048) |
--pot |
Power-of-two page sides; needs a power-of-two --max-size |
--lossless, --output-quality, -c, --dither, --no-bleed |
Page encoding, as in convert. Use --lossless for sprites; PNG and lossless WebP pages get edge bleed under transparent pixels |
--effort N |
WebP encoder effort, as in convert, but the default is 9: an atlas is built once and loaded often. A big lossless sheet comes out about 25% smaller than at 6, and takes several times longer to encode |
--force |
Replace existing pages; files whose bytes didn't change are left untouched |
--json |
Report pages (image, json, size, frames, bytes), frames, inputBytes and bytes |
codex-img pyramid builds zoomed-out levels of a big tiled map, for overview zoom in a game or map viewer. The input is a directory of tiles named x_y.png (x the column, y the row), as TripleA's baseTiles and reliefTiles are. Level 0 is the full map, and each next level is half the size, until a level fits in one tile. Every level is resampled from the full map with premultiplied alpha and cut on the same x_y grid. Tiles at the right and bottom edges are cropped to the map size, so padding in the source edge tiles is dropped. It needs no login and no quota, and the same tiles and options always give the same bytes.
codex-img pyramid map/baseTiles -o web/base/ --map-size 3500x2000 --lossless
codex-img pyramid map/reliefTiles -o web/relief/ --map-size 3500x2000 --merge 512 --output-quality 85The output is <dir>/<level>/x_y.webp plus <dir>/pyramid.json: {"mapWidth", "mapHeight", "tileSize", "format", "levels": [{"level", "width", "height", "cols", "rows", "tiles": {"x_y": "0/x_y.webp"}}]}. A tile with no visible pixel, which is common in relief layers, isn't written or listed. A level's width and height are the map's divided by 2^level, rounded up.
| Option | |
|---|---|
-o DIR |
Output directory (required) |
--map-size WxH |
The map's size (map.width x map.height in TripleA's map.properties); pixels past it are padding. Default: the tiles' extent |
--tile PX |
Input tile size (default 256) |
--merge PX |
Output tile size, a multiple of --tile (512, 1024): fewer, bigger tiles |
--levels N |
At most N levels, level 0 included |
-f FMT |
webp (default) or png |
--lossless, --output-quality, -c, --dither, --no-bleed, --effort |
Tile encoding, as in convert. Flat-colour maps are much smaller with --lossless; painted relief is smaller lossy. --effort 9 shaves a few percent off lossless tiles; on lossy tiles it barely helps |
--force |
Replace an existing pyramid; files whose bytes didn't change are left untouched |
--json |
Report per level (size, tiles, bytes, changed) and totals |
The whole map is assembled in memory, up to 400 million pixels.
codex-img tile makes a panorama wrap around, so it can repeat side by side without a mirror, for skies and backdrops in side-scrolling and racing games. Mirroring a tile shows every landmark twice; tile joins the image's own right edge to its left edge instead. It uses one image of quota.
codex-img tile sky.png -o sky-tile.png --preview /tmp/join.png
codex-img tile sky.png -o sky-tile.png --prompt "a harbour town on a cliff, sailboats, big clouds"How it works:
- The image is rolled by half its width, so the wrap seam moves to the centre.
- The model is asked to repaint a band there so the scenery continues across it. The Codex image endpoint ignores masks, so the whole image comes back redrawn, though only slightly outside that band.
- Only the band is taken from the edit. On each side it's cut along the path where the edit and the original agree best. The cut goes around anything the edit redrew in full, such as a mountain that was split at the edge and comes back whole.
- The spliced pixels are colour-matched to the original, and the image is rolled back.
The result keeps the original's size and framing, every pixel outside the band is unchanged, and the repaired join sits at the left and right edges. --preview writes two copies side by side around the join, so the result can be checked in one image. --keep-edit keeps the model's full edit, and --json reports the repaired band's width.
codex-img batch builds a whole set of assets, a game's art for example, from one JSON spec. It generates each asset's raw image if it's missing (4 at a time), then converts every raw image into its published form. Existing raw images are kept unless you request --reroll; their earlier versions go to history. Converted files are rewritten only when their bytes change, so an unchanged asset stays unchanged in git.
{
"raw_dir": "raw",
"out_dir": "../public/assets",
"style": "16-bit arcade pixel art, bold saturated colours, clean dark outlines, no text.",
"defaults": {"background": "transparent", "hard_alpha": true, "colors": 160, "trim": true},
"assets": {
"trees/oak": {"prompt": "A single big old oak tree.", "aspect": "3:2", "max": [420, 380]},
"harbor/boat": {"prompt": "A fishing boat, side view.", "aspect": "3:2", "max": [420, 300],
"key": "auto", "key_region": "bottom:30%", "key_cut": true, "trim_density": 0.15},
"harbor/sky": {"prompt": "A wide harbour sky panorama.", "aspect": "3:1", "background": "opaque",
"format": "webp", "hard_alpha": false, "colors": null, "trim": false, "output_quality": 85},
"mill/full": {"prompt": "A windmill.", "aspect": "2:3", "publish": false},
"mill/sails": {"prompt": "Edit this image: only the four sails, hub centred.", "aspect": "1:1",
"reference": "mill/full", "max": [400, 400]}
}
}codex-img batch art/assets.json # everything: generate what's missing, convert all
codex-img batch art/assets.json harbor trees/oak # a folder of keys, or one key
codex-img batch art/assets.json --dry-run # what would be generated and converted
codex-img batch art/assets.json --convert-only # no login, no quota- Keys are paths: the raw image goes to
<raw_dir>/<key>.pngand the output to<out_dir>/<key>.<format>. Both directories are relative to the spec file, and default torawandout. styleis appended to every prompt. One shared style sentence keeps a large set looking like one game.defaultsapply to every asset. An asset overrides them, andnullorfalseturns one off, like the sky above.- Palettes:
palette(hex codes as a string or a list, a file relative to the spec, or a palette preset) andpalette_clean. The hex codes join the prompt when the raw image is generated, and the conversion snaps to them. - Presets:
view,styleandcharactername presets, as the options of the same names do;styleandcharactercan be lists.style_ref,character_refandcomposition_refare reference images with a role, relative to the spec. The spec can define its own presets under top-levelviews,stylesandcharacters, which win over the project's and global ones. The top-levelstyleis still plain text appended to every prompt; an asset'sstylenames a preset. - Manifests: each generated raw image gets
<key>.png.jsonbeside it, with the prompt sent, the presets, the inputs with fingerprints, and what the backend reported. When the spec or an input has changed since, the asset'sskipline says so (changed: truewith--json). The asset is never regenerated on its own, because that would spend quota; use--reroll <key>to keep its history. - Generation fields:
prompt,aspect,size,quality,background,reference(other keys whose raw images are passed as-i; they're generated first) andimages(other-ifiles).aspectleads the prompt, ahead of thestyle, and a frame more than 2% off it is reported as a warning on the asset's line.publish: falsegenerates an asset without converting it, for references. - Conversion fields are the
convertoptions with underscores:format,colors,dither,output_quality,lossless,trim,hard_alpha,resize,fit,no_enlarge,nearest,no_bleed,key,key_region,key_spread,key_cutandtrim_density.max: [W, H]is short forresizewithno_enlarge. - Validation: the whole spec is checked before anything runs. An unknown field, a missing reference or a reference loop is an error that names the asset.
- Output: one line per asset and step (
ok,skip,same,FAILED), or one JSON object each with--json. After a login or quota error, no more images are started. The exit code is the worst failure's, so a run with any failure exits non-zero. - Options:
-j Nsets how many images are generated at the same time (default 4, max 10), and--generate-onlyskips the conversion step.
Every generation, from a plain run, batch or tile, writes events as it goes, so other tools can show images while they're being made.
A project is the folder holding the nearest codex-img.json, searched upward from the working folder, as presets are. An empty {} file is enough to mark a project. Saving an image into another folder doesn't change which project the run belongs to.
codex-img init [folder] starts one: it writes {} to codex-img.json in the folder (default: the current one), and --no-events writes {"events": false} instead. It refuses inside an existing project and never overwrites. --json prints {file, root, events}.
Inside a project, each run has its own file:
.codex-img/
README.md
runs/2026-10-02T14-10-08_<run id>.ndjson
Paths inside the project are relative to its root, including cwd, spec, inputs and outputs. Paths outside it stay absolute. Project event files omit the absolute root, so they can travel with the folder. They never rotate. The first run writes .codex-img/README.md explaining that committing this folder is your choice; to keep it local, add .codex-img/ to .gitignore yourself. The CLI never edits .gitignore or replaces your README.
Every generated image in a project gets a manifest beside it (<image>.json), including kept raw originals and tile edits. Reference paths inside the project are relative to the project root. Outside a project, plain runs still need --manifest, batch raw images still get manifests, and tile keeps its previous behavior. Manifests are never replaced; tile --force may replace images, but preserves existing manifests and warns about them.
Outside a project, events still go to $XDG_STATE_HOME/codex-img/events.ndjson, else ~/.local/state/codex-img/events.ndjson (%LOCALAPPDATA%\codex-img\ on Windows). Watch that log with tail -f ~/.local/state/codex-img/events.ndjson. Its paths are absolute, and past 8 MB it moves to events.ndjson.1 and a new log starts.
CODEX_IMG_EVENTS=<path> forces one log with absolute paths, including for project runs. Its run.started includes the absolute project root. CODEX_IMG_EVENTS=off (also 0, false or no) turns off event files and registry updates everywhere; it doesn't turn off manifests.
To turn logging off for good, add "events": false at the top level of a project's codex-img.json: its runs write no run file, no .codex-img/README.md and no registry entry. In the global presets.json ($XDG_CONFIG_HOME/codex-img/, else ~/.config/codex-img/), the same key covers runs outside a project and projects that don't set it. CODEX_IMG_EVENTS wins over both, then the project file, then the global file; the default is on. Manifests are written either way, since rerun, refine and versions depend on them.
A projects.json beside the default global log lists recent projects as [{"root":"/absolute/project", "lastRun":1790949608000}]. lastRun is the run's start time in Unix milliseconds. A project run updates it even when events are sent to an override log. A file lock protects concurrent updates, and a temporary file and rename let readers see a complete list.
New events use format v:2. Version 1 lines keep their original absolute paths. These are example project events:
{"v":2,"t":1790938383960,"event":"run.started","run":"cbb4…","pid":12089,"source":"run","cwd":".","jobs":1,"generation":true}
{"v":2,"t":1790938383970,"event":"job.started","run":"cbb4…","job":"0677…","pid":12089,"prompt":"a small red mushroom","inputs":[{"path":"art/anchor.png","role":"character"}],"request":{"aspect":"1:1","background":"transparent"},"output":"art/mushroom.png"}
{"v":2,"t":1790938383970,"event":"job.stage","run":"cbb4…","job":"0677…","pid":12089,"stage":"editing"}
{"v":2,"t":1790938427182,"event":"job.done","run":"cbb4…","job":"0677…","pid":12089,"path":"art/mushroom.png","rawPath":"art/mushroom.raw.png","manifestPath":"art/mushroom.png.json","size":"734x694","durationMs":40772}
{"v":2,"t":1790938427190,"event":"run.ended","run":"cbb4…","pid":12089,"code":0,"done":1,"failed":0,"durationMs":43230}- Usage: new
run.startedevents includegeneration: truefor backend work andfalsefor local work, including batch restores. Count completed jobs, rather than raw files or planned jobs.job.done.historyPathrecords the previous batch raw’s archive when re-roll or restore replaces it. Re-rolls omitparentfromjob.started;job.doneadds it only after activation succeeds. - Runs and jobs: a run is one command (
source:run,batch,tile,convertorsheet), and its jobs are the images it generates.run.startedsays how manyjobsare planned (and thespecforbatch). Each job writesjob.started,job.stageas the backend reports progress, thenjob.doneorjob.failed(withcode, the exit code it maps to, andmessage).run.endedgives the run'scode, how many jobs weredoneandfailed, and amessagewhen the run itself stopped, a login error before any job for example. Every event carries itsrunid, and job events theirjobid. - Job fields:
promptas given,submittedPromptwhen presets or labels changed it,inputswith their roles, therequestsettings, and the asked-foroutput.batchadds the asset'skey, and-nadds the job'sindex. - Interrupted runs: a killed process writes no end, so a reader should check whether the run's
pidis still alive. - Never in the way: event or registry write errors are ignored, and generation goes on. Each event is one line written at once, so parallel jobs don't mix their lines.
- Prompts are stored: event files keep every prompt in plain text, as manifests do.
The Codex image endpoint isn't a public API, and nothing about its behaviour is documented. This section separates what codex-img controls from what has been observed in use (as of September and October 2026). The observations can change whenever the backend does, without a codex-img release.
What codex-img controls
- The request. By default
codex-imgdoes what the Codex CLI does: it sends{prompt, model: "gpt-image-2", size, quality, background}to/images/generations, or to/images/editswithimages: [{image_url: "data:…"}]. Your prompt reaches the image model unchanged, apart from the one framing sentence--aspectputs in front of it.--via-responsesuses the Responses API with theimage_generationtool instead (ported from pi-codex-image-gen), where a routing model may rewrite the prompt. - Input checks. The endpoint doesn't validate its inputs and ignores values it doesn't know, so
codex-imgchecks every option before sending anything, and checks that the output file is free before spending quota. - Everything after the image arrives: conversion, trimming, resizing, palettes, background removal, and never overwriting a file. This runs locally, is covered by tests, and doesn't depend on the backend.
What has been observed, not guaranteed
- Model. On the direct route,
codex-imgsendsmodel: "gpt-image-2", as the Codex CLI does, and the response doesn't say which model made the image. The Responses route has reportedgpt-image-2-codex. Which model serves each route, and whether that changes, isn't known.--modelonly picks the routing model for--via-responses. - Shape comes from the prompt, not
size. In October 2026 tests, the same prompt withsize: 1024x1536and withsize: 1536x1024both came back at 1312x1199. Leading the prompt with a ratio sentence (what--aspectdoes) gave that ratio every time, on generations and on edits: an edit of a 2:3 image with-a 16:9came back 16:9, with the scene widened around the subject. The pixel count stays around 1.57 megapixels, so 2:3 came back at exactly 1024x1536 and 16:9 at 1672x941. Other tools have reported 3:4 at 1086x1448, 9:16 at 941x1672, 21:9 at 1916x821, and anything beyond 3:1 clamped to 3:1. Without--aspect, shape words in the prompt ("wide landscape") steer it too, less precisely. Add--resize WxH --fit coverto get exact pixels. - Quality is capped at medium on the subscription. Prompts led by a ratio sentence came back reported as
low, with fewer image tokens (343 for 2:3 and 301 for 16:9, against 829 without it), even with-q medium; the images still looked fully detailed. Why isn't known. - The
modelfield isn't checked. Sendinggpt-image-2.5-sunburstinstead ofgpt-image-2was accepted without an error, and gave the same size, quality and token count, so there's no sign it picked another model. - Transparency.
background: transparentgives real alpha, but "solid" pixels come back at alpha 250–254 (--hard-alphafixes that), and the model sometimes paints a background anyway (--keyremoves it). - Views (built-in camera presets, October 2026, flat-coloured game sprites of a cottage, a street lamp, a car and a tree).
side,front,isometricandtop-downcame out as asked for the car and the tree, andside/frontgave flat elevations for the cottage and the lamp. A building seentop-downstill showed a sliver of its front wall. The first wording ofthree-quarterturned the cottage 45° (isometric-looking), and the firsttop-downgave a front view with a big roof; the built-in texts now say "front square to the camera, not turned" and "like a map: for a building, only its roof", which fixed both. That text first said "as in 16-bit RPGs", which twice in a row turned a car into pixel art: a camera preset was setting the style. The phrase is gone, and the same car came out flat like the rest. - Reference images become part of the scene. Any input image goes to
/images/edits, so the result takes that image's frame (a 2:3 style reference gave a 2:3 result;-aoverrides it). A style reference photo of an apple on a table gave a fox on that same table: describe the new setting in the prompt. In two A/B pairs, the role labels made no visible difference next to a plain-i; they're there so several images can't be confused. - Character text leaks into scenes. A character preset whose text was a whole sprite prompt ("isolated on a transparent background, standing") gave scenes that faded to transparent at the edges. With only the character's looks as text, the same scenes filled the frame and the character kept its face, outfit and colours.
- Palettes. Treasure-chest sprites, October 2026. Hex codes in the prompt put 81–88% of pixels within distance 16 (RGB) of a palette colour and 97–99% within 40, yet every image still had 9,000–14,000 distinct colours. A palette's name alone ("PICO-8") gave 8% within 16, and "a limited palette of 8 colours" changed nothing. A swatch image as a style reference didn't beat the hex codes. Listing PICO-8's colours, or naming it, made the model switch to pixel art by itself. Snapping in OKLab after the prompt changed little visibly (0.1–1% stray pixels before the despeckle). Snapping an image generated without the palette gave purple streaks in brown wood, which
--palette-cleanturns into the palette's neutral grey. With 64 colours (Resurrect 64, AAP-64), the prompt mattered less: 33–49% of pixels within 16 (as many distinct colours as with no palette), though the design still took the palette's greys and purples. Because a 64-colour palette covers most shades, the plain snap looked right even on an image generated without it. - Masks are accepted and ignored: requests with and without one used the same number of input tokens.
tileworks around this. - No seed. An image can't be generated again exactly, which is why
codex-imgkeeps the untouched original whenever it changes pixels.
Reported, not requested. In --json, imageModel, quality, background and size are what the backend reported in its response, not what codex-img asked for, and they aren't checked against the image. The exception is size after --trim, --resize or --hard-alpha: then it's measured from the saved file, and the backend's value moves to rawSize. usage is the token counts the backend reported.
Quota. Requests run on your ChatGPT subscription login, but how they count against your plan's limits isn't known: the endpoint doesn't say which allowance a request uses, what it costs or how much is left, so codex-img can't either. When the backend answers with a recognised quota or usage-limit error, codex-img exits with code 3 and batch stops starting new images. batch --dry-run lists what would be generated before anything is sent.
codex-imgonly readsauth.json. It never refreshes or writes tokens, so it can't interfere with yourcodexlogin. If the login has expired or gets rejected, it exits with code2and tells you to open Codex (thecodexCLI or the app) so it renews the login, or to runcodex login.CODEX_HOMEoverrides~/.codex.- Debugging:
CODEX_IMG_DEBUG_RAW=/tmp/raw.txt codex-img …saves the raw response body (the JSON, or the event stream with--via-responses). It contains the full base64 image.
The request format and stream parser are ported from pi-codex-image-gen (Apache-2.0). See NOTICE.